From cb997fd0280aac12afdbdbe06719829975033416 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Fri, 9 Oct 2026 08:47:10 +0530 Subject: [PATCH 001/249] Probe: Claude's documented control requests over the managed stream-json transport Records the initialize metadata (model catalog, commands, agents), get_context_usage, mcp_status, set_model, apply_flag_settings (effort), set_permission_mode and the runtime facts that confirm each change, in a disposable workspace with customizations disabled and a budget cap. Account identity values are never printed. --- scripts/probes/claude-controls.mjs | 151 +++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 scripts/probes/claude-controls.mjs diff --git a/scripts/probes/claude-controls.mjs b/scripts/probes/claude-controls.mjs new file mode 100644 index 00000000..6cf99360 --- /dev/null +++ b/scripts/probes/claude-controls.mjs @@ -0,0 +1,151 @@ +/** + * Disposable probe of Claude Code's documented control requests over the same stream-json transport NMSh's managed + * adapter uses (the requests the official Agent SDK's Query methods send: initialize, get_context_usage, mcp_status, + * set_model, apply_flag_settings, set_permission_mode). It records shapes, acknowledgements and the runtime facts + * that confirm each change; it never prints account identity values, credentials or prompts beyond fixed probe text. + * + * NMSH_PROBE_CLAUDE=/path/to/claude CLAUDE_CONFIG_DIR=/path/to/config node scripts/probes/claude-controls.mjs + */ +import {spawn, spawnSync} from 'node:child_process'; +import {mkdtempSync, writeFileSync, readFileSync, rmSync} from 'node:fs'; +import {join} from 'node:path'; +import {tmpdir} from 'node:os'; +import {randomUUID} from 'node:crypto'; + +const executable = process.env.NMSH_PROBE_CLAUDE ?? 'claude'; +const version = spawnSync(executable, ['--version'], {encoding: 'utf8', timeout: 5000}).stdout.trim(); +const root = mkdtempSync(join(tmpdir(), 'nmsh-claude-controls-')); +const facts = {version, steps: []}; +const note = (step, value) => { facts.steps.push({step, ...value}); console.log(JSON.stringify({step, ...value})); }; + +const argv = ['--print', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose', + '--permission-prompts', 'host', '--permission-prompt-tool', 'stdio', '--setting-sources', '', '--strict-mcp-config', + '--mcp-config', '{"mcpServers":{}}', '--max-budget-usd', '0.5', '--safe-mode', '--no-session-persistence', + '--include-partial-messages', '--model', 'haiku', '--tools', 'Read,Edit']; + +writeFileSync(join(root, 'fixture.txt'), 'alpha\nbeta\ngamma\n', {mode: 0o600}); +const child = spawn(executable, argv, {cwd: root, stdio: ['pipe', 'pipe', 'pipe']}); +const kill = setTimeout(() => { note('timeout', {}); child.kill('SIGKILL'); }, 150000); +let stderr = ''; +child.stderr.on('data', chunk => { stderr = (stderr + chunk).slice(-2000); }); + +const pending = new Map(); +const waiters = []; +const seen = []; +let buffer = ''; +child.stdout.on('data', chunk => { + buffer += chunk; + let index; + while ((index = buffer.indexOf('\n')) >= 0) { + const line = buffer.slice(0, index); buffer = buffer.slice(index + 1); + let event; try { event = JSON.parse(line); } catch { continue; } + seen.push(event); + if (event.type === 'control_response') { + const waiter = pending.get(event.response?.request_id); + if (waiter) { pending.delete(event.response.request_id); waiter(event.response); } + } else if (event.type === 'control_request' && event.request?.subtype === 'can_use_tool') { + note('permission-request', {tool: event.request.tool_name, inputKeys: Object.keys(event.request.input ?? {})}); + child.stdin.write(JSON.stringify({type: 'control_response', response: {subtype: 'success', request_id: event.request_id, + response: {behavior: 'allow', updatedInput: event.request.input}}}) + '\n'); + } + for (const waiter of [...waiters]) if (waiter.match(event)) { waiters.splice(waiters.indexOf(waiter), 1); waiter.resolve(event); } + } +}); + +const request = (body, timeout = 30000) => new Promise(resolve => { + const id = randomUUID(); + const timer = setTimeout(() => { pending.delete(id); resolve({subtype: 'timeout'}); }, timeout); + pending.set(id, response => { clearTimeout(timer); resolve(response); }); + child.stdin.write(JSON.stringify({type: 'control_request', request_id: id, request: body}) + '\n'); +}); +const until = (match, timeout = 90000) => new Promise(resolve => { + const waiter = {match, resolve}; + waiters.push(waiter); + setTimeout(() => { const at = waiters.indexOf(waiter); if (at >= 0) { waiters.splice(at, 1); resolve(undefined); } }, timeout); +}); +const turn = async text => { + const start = seen.length; + child.stdin.write(JSON.stringify({type: 'user', message: {role: 'user', content: [{type: 'text', text}]}}) + '\n'); + const result = await until(event => event.type === 'result'); + return {events: seen.slice(start), result}; +}; +const shape = value => value && typeof value === 'object' ? Object.keys(value).sort() : typeof value; +const summarizeTurn = ({events, result}) => { + const types = [...new Set(events.map(event => `${event.type}${event.subtype ? `/${event.subtype}` : ''}`))]; + const init = events.find(event => event.type === 'system' && event.subtype === 'init'); + const streamKinds = [...new Set(events.filter(event => event.type === 'stream_event').map(event => event.event?.type))]; + const assistantModels = [...new Set(events.filter(event => event.type === 'assistant').map(event => event.message?.model))]; + const toolResults = events.filter(event => event.type === 'user' && event.tool_use_result !== undefined).map(event => shape(event.tool_use_result)); + const patch = events.find(event => event.type === 'user' && event.tool_use_result?.structuredPatch)?.tool_use_result?.structuredPatch; + const rateLimits = events.filter(event => event.type === 'rate_limit_event').map(event => ({status: event.rate_limit_info?.status, + type: event.rate_limit_info?.rateLimitType, utilization: event.rate_limit_info?.utilization, hasReset: typeof event.rate_limit_info?.resetsAt === 'number'})); + return {types, init: init && {model: init.model, effort: init.effort, permissionMode: init.permissionMode, capabilities: init.capabilities, + slashCommands: init.slash_commands?.length, tools: init.tools, outputStyle: init.output_style, viewMode: init.view_mode}, + streamKinds, assistantModels, toolResults, patch, rateLimits, + result: result && {subtype: result.subtype, isError: result.is_error, numTurns: result.num_turns, costUsd: result.total_cost_usd, + usage: result.usage && {input: result.usage.input_tokens, output: result.usage.output_tokens, cacheRead: result.usage.cache_read_input_tokens, cacheWrite: result.usage.cache_creation_input_tokens}, + modelUsage: result.modelUsage && Object.fromEntries(Object.entries(result.modelUsage).map(([model, usage]) => [model, {contextWindow: usage.contextWindow, + maxOutputTokens: usage.maxOutputTokens, input: usage.inputTokens, output: usage.outputTokens, cacheRead: usage.cacheReadInputTokens}])), + text: typeof result.result === 'string' ? result.result.slice(0, 120) : undefined}}; +}; + +try { + const init = await request({subtype: 'initialize', hooks: {}}); + const body = init.response ?? {}; + note('initialize', {ack: init.subtype, keys: shape(body), + models: (body.models ?? []).map(model => ({value: model.value, resolved: model.resolvedModel, name: model.displayName, effort: model.supportsEffort, + levels: model.supportedEffortLevels, fast: model.supportsFastMode, auto: model.supportsAutoMode})), + commands: {count: body.commands?.length, sample: (body.commands ?? []).slice(0, 60).map(command => `${command.name}${command.builtin ? '*' : ''}${command.argumentHint ? ` ${command.argumentHint}` : ''}`)}, + agents: (body.agents ?? []).map(agent => agent.name), outputStyle: body.output_style, styles: body.available_output_styles, + accountKeys: shape(body.account), apiProvider: body.account?.apiProvider, subscriptionType: body.account?.subscriptionType}); + + const usageBefore = await request({subtype: 'get_context_usage', detail: 'summary'}); + const usage = usageBefore.response ?? {}; + note('context-before-turn', {ack: usageBefore.subtype, error: usageBefore.error, keys: shape(usage), totalTokens: usage.totalTokens, maxTokens: usage.maxTokens, + rawMaxTokens: usage.rawMaxTokens, percentage: usage.percentage, model: usage.model, autoCompactThreshold: usage.autoCompactThreshold, isAutoCompactEnabled: usage.isAutoCompactEnabled, + categories: (usage.categories ?? []).map(category => ({name: category.name, tokens: category.tokens, kind: category.kind})), + memoryFiles: (usage.memoryFiles ?? []).length, mcpTools: (usage.mcpTools ?? []).length, systemTools: (usage.systemTools ?? []).length, + skills: usage.skills && {total: usage.skills.totalSkills, included: usage.skills.includedSkills, tokens: usage.skills.tokens}, apiUsage: usage.apiUsage}); + + const mcp = await request({subtype: 'mcp_status'}); + note('mcp-status', {ack: mcp.subtype, keys: shape(mcp.response), servers: (mcp.response?.mcpServers ?? []).map(server => ({name: server.name, status: server.status}))}); + + const first = await turn('Reply with exactly: PROBE-ONE'); + note('turn-haiku', summarizeTurn(first)); + + const model = await request({subtype: 'set_model', model: 'sonnet'}); + note('set-model', {ack: model.subtype, error: model.error, response: model.response}); + const effort = await request({subtype: 'apply_flag_settings', settings: {effortLevel: 'low'}}); + note('apply-effort', {ack: effort.subtype, error: effort.error, response: effort.response}); + const mode = await request({subtype: 'set_permission_mode', mode: 'acceptEdits'}); + note('set-permission-mode', {ack: mode.subtype, error: mode.error, response: mode.response}); + + const second = await turn('Read fixture.txt, then use Edit to replace beta with BETA. Reply DONE when finished.'); + note('turn-after-changes', summarizeTurn(second)); + note('fixture-after', {content: readFileSync(join(root, 'fixture.txt'), 'utf8')}); + + const usageAfter = await request({subtype: 'get_context_usage', detail: 'summary'}); + const after = usageAfter.response ?? {}; + note('context-after-turns', {ack: usageAfter.subtype, totalTokens: after.totalTokens, maxTokens: after.maxTokens, percentage: after.percentage, model: after.model, + categories: (after.categories ?? []).map(category => ({name: category.name, tokens: category.tokens, kind: category.kind})), + messageBreakdown: after.messageBreakdown && {toolCall: after.messageBreakdown.toolCallTokens, toolResult: after.messageBreakdown.toolResultTokens, + assistant: after.messageBreakdown.assistantMessageTokens, user: after.messageBreakdown.userMessageTokens, + byType: after.messageBreakdown.toolCallsByType}, apiUsage: after.apiUsage}); + + const slash = await turn('/context'); + note('slash-context', {types: [...new Set(slash.events.map(event => `${event.type}${event.subtype ? `/${event.subtype}` : ''}`))], + localOutput: slash.events.filter(event => event.subtype === 'local_command_output' || event.type === 'local_command_output').map(event => shape(event)), + result: slash.result && {subtype: slash.result.subtype, isError: slash.result.is_error, costUsd: slash.result.total_cost_usd, text: String(slash.result.result ?? '').slice(0, 160)}}); + + const reset = await request({subtype: 'apply_flag_settings', settings: {effortLevel: null}}); + note('reset-effort', {ack: reset.subtype, error: reset.error}); +} catch (error) { + note('error', {message: String(error?.message ?? error).slice(0, 300)}); +} finally { + child.stdin.end(); + await new Promise(resolve => { child.once('close', resolve); setTimeout(resolve, 8000).unref(); }); + clearTimeout(kill); + child.kill('SIGKILL'); + if (stderr.trim()) note('stderr-tail', {text: stderr.slice(-400)}); + rmSync(root, {recursive: true, force: true}); +} From 5d02198e7934c3eee332ce228744526385fd415e Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Fri, 9 Oct 2026 08:47:10 +0530 Subject: [PATCH 002/249] Markdown for untrusted text: blocks, inline styles, code highlighting and bounded rows Agent replies, provider command output and GitHub bodies are Markdown from outside NMSh. This renders them as terminal rows that always fit their width: headings, nested and task lists, quotes, GFM tables that wrap cells (records when far too narrow), fenced code with a gutter and language-aware highlighting from the person's syntax roles, and rules. Links become OSC 8 hyperlinks only for http(s) targets and always show where they go; refused targets stay visible as text. Safe glyphs and NO_COLOR keep the structure with ASCII markers and backticks. The authored-only /help renderer is unchanged. --- src/markdown/blocks.ts | 220 +++++++++++++++++++++ src/markdown/highlight.ts | 238 +++++++++++++++++++++++ src/markdown/inline.ts | 289 +++++++++++++++++++++++++++ src/markdown/render.ts | 364 +++++++++++++++++++++++++++++++++++ tests/markdownRender.test.ts | 180 +++++++++++++++++ 5 files changed, 1291 insertions(+) create mode 100644 src/markdown/blocks.ts create mode 100644 src/markdown/highlight.ts create mode 100644 src/markdown/inline.ts create mode 100644 src/markdown/render.ts create mode 100644 tests/markdownRender.test.ts diff --git a/src/markdown/blocks.ts b/src/markdown/blocks.ts new file mode 100644 index 00000000..d3164959 --- /dev/null +++ b/src/markdown/blocks.ts @@ -0,0 +1,220 @@ +/** + * Block structure of untrusted Markdown: headings, paragraphs, fenced and indented code, lists (nested, ordered, + * task items), block quotes, GFM tables and thematic breaks. A pragmatic subset of CommonMark/GFM tuned for agent + * replies and GitHub text, not a conformance implementation. Streaming-safe: an unclosed fence runs to the end and + * is marked open, so a half-arrived reply still reads as code. Bounded in depth and lines; tabs are expected to be + * expanded by the caller's display scrubber. + */ + +export type Alignment = 'left' | 'center' | 'right' | undefined; + +export type Block = + | {kind: 'heading'; level: number; text: string} + | {kind: 'paragraph'; text: string} + | {kind: 'code'; language?: string; lines: string[]; closed: boolean} + | {kind: 'list'; ordered: boolean; start: number; loose: boolean; items: ListItem[]} + | {kind: 'quote'; blocks: Block[]} + | {kind: 'table'; align: Alignment[]; header: string[]; rows: string[][]} + | {kind: 'rule'}; + +export interface ListItem { + /** `•`-style bullets are drawn by the renderer; ordered items keep their number and delimiter. */ + number?: number; + task?: 'open' | 'done'; + blocks: Block[]; +} + +const MAX_DEPTH = 12; +const MAX_LINES = 50_000; + +const FENCE = /^( {0,3})(`{3,}|~{3,})[ \t]*([^`]*?)[ \t]*$/u; +const HEADING = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?(?:[ \t]+#+)?[ \t]*$/u; +const RULE = /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/u; +const QUOTE = /^ {0,3}>[ ]?/u; +const ITEM = /^( {0,3})([-+*]|\d{1,9}[.)])([ \t]+|$)/u; +const SETEXT = /^ {0,3}(=+|-+)[ \t]*$/u; +const DELIMITER_CELL = /^:?-+:?$/u; + +const blank = (line: string) => line.trim() === ''; +const indentOf = (line: string) => line.length - line.trimStart().length; + +/** Cells of a pipe table row; escaped pipes stay text, and pipes inside code spans do not split. */ +export function splitRow(line: string): string[] { + let text = line.trim(); + if (text.startsWith('|')) text = text.slice(1); + if (text.endsWith('|') && !text.endsWith('\\|')) text = text.slice(0, -1); + const cells: string[] = []; + let cell = '', code = 0; + for (let index = 0; index < text.length; index++) { + const char = text[index]!; + if (char === '\\' && text[index + 1] === '|') { cell += '|'; index++; continue; } + if (char === '`') code = code ? 0 : 1; + if (char === '|' && !code) { cells.push(cell.trim()); cell = ''; continue; } + cell += char; + } + cells.push(cell.trim()); + return cells; +} + +function delimiterRow(line: string): Alignment[] | undefined { + if (!line.includes('-') || (!line.includes('|') && !/^\s*:?-+:?\s*$/u.test(line))) return undefined; + const cells = splitRow(line); + if (!cells.length || !cells.every(cell => DELIMITER_CELL.test(cell))) return undefined; + return cells.map(cell => cell.startsWith(':') && cell.endsWith(':') ? 'center' : cell.endsWith(':') ? 'right' : cell.startsWith(':') ? 'left' : undefined); +} + +/** Whether a line starts a block that interrupts a paragraph (a lazy continuation line must not). */ +function interrupts(line: string, next: string | undefined): boolean { + if (FENCE.test(line) || HEADING.test(line) || RULE.test(line) || QUOTE.test(line)) return true; + const item = ITEM.exec(line); + // An ordered item interrupts a paragraph only when it starts at 1, and an empty item never does (CommonMark). + if (item && line.slice(item[0].length).trim() && (!/\d/u.test(item[2]!) || /^1[.)]$/u.test(item[2]!))) return true; + return Boolean(line.includes('|') && next !== undefined && delimiterRow(next) && splitRow(line).length === delimiterRow(next)!.length); +} + +export function parseBlocks(source: string): Block[] { + const lines = source.replace(/\r\n?/gu, '\n').split('\n'); + if (lines.length > MAX_LINES) lines.length = MAX_LINES; + return parseLines(lines, 0); +} + +function parseLines(lines: readonly string[], depth: number): Block[] { + const blocks: Block[] = []; + if (depth > MAX_DEPTH) { + const text = lines.join('\n').trim(); + return text ? [{kind: 'paragraph', text}] : []; + } + let index = 0; + while (index < lines.length) { + const line = lines[index]!; + if (blank(line)) { index++; continue; } + + const fence = FENCE.exec(line); + if (fence && !(fence[2]!.startsWith('`') && fence[3]!.includes('`'))) { + const marker = fence[2]!; + const indent = fence[1]!.length; + const code: string[] = []; + let closed = false; + for (index++; index < lines.length; index++) { + const candidate = lines[index]!; + const close = /^ {0,3}(`{3,}|~{3,})[ \t]*$/u.exec(candidate); + if (close && close[1]![0] === marker[0] && close[1]!.length >= marker.length) { closed = true; index++; break; } + // Content keeps its own indentation, less the fence's. + code.push(candidate.slice(Math.min(indent, indentOf(candidate)))); + } + const language = fence[3]!.split(/[\s{,]/u)[0]?.toLowerCase() || undefined; + blocks.push({kind: 'code', ...(language ? {language} : {}), lines: code, closed}); + continue; + } + + const heading = HEADING.exec(line); + if (heading) { blocks.push({kind: 'heading', level: heading[1]!.length, text: (heading[2] ?? '').trim()}); index++; continue; } + + // A thematic break wins over a list item (`* * *`). + if (RULE.test(line)) { blocks.push({kind: 'rule'}); index++; continue; } + + if (QUOTE.test(line)) { + const inner: string[] = []; + while (index < lines.length) { + const candidate = lines[index]!; + if (QUOTE.test(candidate)) inner.push(candidate.replace(QUOTE, '')); + // Lazy continuation: a plain text line continues the quote's paragraph. + else if (!blank(candidate) && inner.length && !blank(inner.at(-1)!) && !interrupts(candidate, lines[index + 1])) inner.push(candidate); + else break; + index++; + } + blocks.push({kind: 'quote', blocks: parseLines(inner, depth + 1)}); + continue; + } + + const item = ITEM.exec(line); + if (item) { + const parsed = parseList(lines, index, depth); + blocks.push(parsed.block); + index = parsed.next; + continue; + } + + const alignment = line.includes('|') ? delimiterRow(lines[index + 1] ?? '') : undefined; + if (alignment) { + const header = splitRow(line); + if (header.length === alignment.length) { + const rows: string[][] = []; + for (index += 2; index < lines.length && !blank(lines[index]!) && lines[index]!.includes('|') && !interrupts(lines[index]!.replace(/\|/gu, ''), undefined); index++) { + const cells = splitRow(lines[index]!); + rows.push(Array.from({length: header.length}, (_, column) => cells[column] ?? '')); + } + blocks.push({kind: 'table', align: alignment, header, rows}); + continue; + } + } + + // Indented code: four spaces, never continuing a paragraph. + if (indentOf(line) >= 4) { + const code: string[] = []; + while (index < lines.length && (indentOf(lines[index]!) >= 4 || blank(lines[index]!))) code.push(lines[index++]!.slice(4)); + while (code.length && blank(code.at(-1)!)) code.pop(); + blocks.push({kind: 'code', lines: code, closed: true}); + continue; + } + + const paragraph: string[] = [line.trim()]; + for (index++; index < lines.length; index++) { + const candidate = lines[index]!; + if (blank(candidate)) break; + const setext = SETEXT.exec(candidate); + if (setext) { + blocks.push({kind: 'heading', level: setext[1]![0] === '=' ? 1 : 2, text: paragraph.join(' ')}); + paragraph.length = 0; + index++; + break; + } + if (interrupts(candidate, lines[index + 1])) break; + // A hard line break (two trailing spaces or a backslash) keeps the break. + const previous = paragraph.at(-1)!; + if (/ {2,}$|\\$/u.test(lines[index - 1]!)) paragraph[paragraph.length - 1] = `${previous.replace(/\\$/u, '')}\n`; + paragraph.push(candidate.trim()); + } + if (paragraph.length) blocks.push({kind: 'paragraph', text: paragraph.join('\n').replace(/\n\n/gu, '\n')}); + } + return blocks; +} + +function parseList(lines: readonly string[], start: number, depth: number): {block: Extract; next: number} { + const first = ITEM.exec(lines[start]!)!; + const ordered = /\d/u.test(first[2]!); + const delimiter = ordered ? first[2]!.slice(-1) : first[2]!; + const items: ListItem[] = []; + let loose = false; + let index = start; + let sawBlankBetween = false; + while (index < lines.length) { + const match = ITEM.exec(lines[index]!); + if (!match) break; + const sameKind = ordered ? /\d/u.test(match[2]!) && match[2]!.slice(-1) === delimiter : match[2] === delimiter; + if (!sameKind || match[1]!.length > first[1]!.length + 1) break; + if (sawBlankBetween) loose = true; + const marker = match[1]!.length + match[2]!.length; + const spacing = match[3]!.length; + // Content starts after one to four spaces; more means the content itself is indented (code), so one space counts. + const width = marker + (spacing >= 1 && spacing <= 4 ? spacing : 1); + const body: string[] = [lines[index]!.slice(width)]; + let blankRun = 0; + for (index++; index < lines.length; index++) { + const candidate = lines[index]!; + if (blank(candidate)) { blankRun++; body.push(''); continue; } + if (indentOf(candidate) >= width) { body.push(candidate.slice(width)); blankRun = 0; continue; } + // Lazy continuation of the item's paragraph text. + if (!blankRun && !interrupts(candidate, lines[index + 1]) && !ITEM.test(candidate)) { body.push(candidate.trim()); continue; } + break; + } + while (body.length && blank(body.at(-1)!)) body.pop(); + // Items separated by blank lines read as a loose list (a blank row between items). + sawBlankBetween = blankRun > 0; + let task: ListItem['task']; + const taskMatch = /^\[([ xX])\][ \t]+/u.exec(body[0] ?? ''); + if (taskMatch) { task = taskMatch[1] === ' ' ? 'open' : 'done'; body[0] = body[0]!.slice(taskMatch[0].length); } + items.push({...(ordered ? {number: Number(match[2]!.slice(0, -1))} : {}), ...(task ? {task} : {}), blocks: parseLines(body, depth + 1)}); + } + return {block: {kind: 'list', ordered, start: items[0]?.number ?? 1, loose, items}, next: index}; +} diff --git a/src/markdown/highlight.ts b/src/markdown/highlight.ts new file mode 100644 index 00000000..5b41c95d --- /dev/null +++ b/src/markdown/highlight.ts @@ -0,0 +1,238 @@ +/** + * Lightweight, bounded syntax highlighting for code blocks in agent replies and diffs. It tokenizes with per-language + * rules (comments, strings, numbers, keywords, types, calls, tags, keys) and carries block-comment and multi-line + * string state across lines. It is presentation only: the text of every line is returned unchanged, unknown + * languages stay plain, and very large blocks highlight only their beginning. No grammar is loaded and nothing runs. + */ + +export type CodeTokenKind = 'plain' | 'keyword' | 'string' | 'number' | 'comment' | 'function' | 'type' | 'operator' | 'property' | 'tag' + | 'constant' | 'inserted' | 'deleted' | 'meta' | 'heading' | 'variable'; + +export interface CodeToken { + text: string; + kind: CodeTokenKind; +} + +const MAX_LINES = 2000; +const MAX_LINE_LENGTH = 4000; + +interface Grammar { + line?: readonly string[]; + block?: ReadonlyArray; + quotes?: readonly string[]; + /** Strings that may span lines (template literals, triple quotes). */ + multiline?: readonly string[]; + keywords?: ReadonlySet; + constants?: ReadonlySet; + /** Capitalized identifiers read as types. */ + types?: boolean; + caseInsensitive?: boolean; + variables?: boolean; + decorators?: boolean; +} + +const words = (text: string) => new Set(text.split(/\s+/u).filter(Boolean)); +const C_CONSTANTS = words('true false null undefined NaN Infinity nullptr nil None True False self this super'); + +const JS = words(`abstract as async await break case catch class const continue debugger declare default delete do else enum export extends finally for + from function get if implements import in infer instanceof interface is keyof let module namespace new of package private protected public readonly + return satisfies set static switch throw try type typeof var void while with yield`); +const PY = words(`and as assert async await break class continue def del elif else except finally for from global if import in is lambda match case + nonlocal not or pass raise return try while with yield`); +const RUST = words(`as async await break const continue crate dyn else enum extern fn for if impl in let loop match mod move mut pub ref return + self Self static struct super trait type unsafe use where while`); +const GO = words(`break case chan const continue default defer else fallthrough for func go goto if import interface map package range return + select struct switch type var`); +const C_LIKE = words(`abstract auto bool break case catch char class const constexpr continue default delete do double else enum explicit export + extends extern final finally float for friend goto if implements import inline instanceof int interface long namespace native new operator + override package private protected public register return short signed sizeof static struct super switch synchronized template this throw + throws transient try typedef typename union unsigned using var virtual void volatile while fun val when object companion data sealed + internal lateinit suspend let guard func init deinit protocol extension import struct mutating weak`); +const RUBY = words('alias and begin break case class def defined? do else elsif end ensure false for if in module next nil not or redo rescue retry return self super then true undef unless until when while yield require attr_accessor'); +const SHELL = words('if then else elif fi for while until do done case esac in function select time return export local readonly declare unset source alias set shift exit'); +const SQL = words(`select from where and or not insert into values update set delete create table index view drop alter add column primary key + foreign references join left right inner outer full on group by order having limit offset as distinct union all case when then else end is null + like in exists between returning with begin commit rollback transaction default unique check constraint`); +const LUA = words('and break do else elseif end false for function goto if in local nil not or repeat return then true until while'); + +const GRAMMARS: Record = { + js: {line: ['//'], block: [['/*', '*/']], quotes: ['"', "'"], multiline: ['`'], keywords: JS, constants: C_CONSTANTS, types: true, decorators: true}, + py: {line: ['#'], quotes: ['"', "'"], multiline: ['"""', "'''"], keywords: PY, constants: C_CONSTANTS, types: true, decorators: true}, + rust: {line: ['//'], block: [['/*', '*/']], quotes: ['"'], keywords: RUST, constants: C_CONSTANTS, types: true, decorators: false}, + go: {line: ['//'], block: [['/*', '*/']], quotes: ['"', "'"], multiline: ['`'], keywords: GO, constants: C_CONSTANTS, types: true}, + c: {line: ['//'], block: [['/*', '*/']], quotes: ['"', "'"], keywords: C_LIKE, constants: C_CONSTANTS, types: true, decorators: true}, + ruby: {line: ['#'], quotes: ['"', "'"], keywords: RUBY, constants: C_CONSTANTS, types: true}, + shell: {line: ['#'], quotes: ['"', "'"], keywords: SHELL, variables: true}, + sql: {line: ['--'], block: [['/*', '*/']], quotes: ["'", '"'], keywords: SQL, caseInsensitive: true, constants: words('true false null')}, + lua: {line: ['--'], block: [['--[[', ']]']], quotes: ['"', "'"], keywords: LUA}, + css: {block: [['/*', '*/']], quotes: ['"', "'"]}, + json: {quotes: ['"'], constants: words('true false null')}, + yaml: {line: ['#'], quotes: ['"', "'"], constants: words('true false null yes no on off ~')}, + toml: {line: ['#'], quotes: ['"', "'"], multiline: ['"""', "'''"], constants: words('true false')}, + markup: {block: [['']], quotes: ['"', "'"]}, +}; + +const ALIASES: Record = { + javascript: 'js', jsx: 'js', mjs: 'js', cjs: 'js', ts: 'js', typescript: 'js', tsx: 'js', mts: 'js', cts: 'js', node: 'js', deno: 'js', + python: 'py', py3: 'py', python3: 'py', pyi: 'py', rs: 'rust', golang: 'go', + 'c++': 'c', cpp: 'c', cc: 'c', cxx: 'c', h: 'c', hpp: 'c', hh: 'c', 'objective-c': 'c', objc: 'c', cs: 'c', csharp: 'c', java: 'c', kotlin: 'c', + kt: 'c', kts: 'c', swift: 'c', scala: 'c', dart: 'c', php: 'c', zig: 'c', groovy: 'c', gradle: 'c', proto: 'c', protobuf: 'c', + rb: 'ruby', sh: 'shell', bash: 'shell', zsh: 'shell', fish: 'shell', ksh: 'shell', console: 'shell', shellsession: 'shell', terminal: 'shell', + dockerfile: 'shell', docker: 'shell', makefile: 'shell', make: 'shell', psql: 'sql', mysql: 'sql', postgres: 'sql', sqlite: 'sql', postgresql: 'sql', + scss: 'css', sass: 'css', less: 'css', jsonc: 'json', json5: 'json', yml: 'yaml', ini: 'toml', cfg: 'toml', conf: 'toml', editorconfig: 'toml', + html: 'markup', xml: 'markup', svg: 'markup', vue: 'markup', svelte: 'markup', xhtml: 'markup', plist: 'markup', + diff: 'diff', patch: 'diff', udiff: 'diff', md: 'markdown', markdown: 'markdown', hcl: 'toml', tf: 'toml', nix: 'shell', ps1: 'shell', powershell: 'shell', +}; + +/** The grammar id for a fence info word, or undefined for plain text. */ +export function languageId(language: string | undefined): string | undefined { + if (!language) return undefined; + const id = language.toLowerCase(); + return GRAMMARS[id] || id === 'diff' || id === 'markdown' ? id : ALIASES[id]; +} + +const NUMBER = /^(?:0[xX][0-9a-fA-F_]+|0[bB][01_]+|0[oO][0-7_]+|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d+)?)[a-zA-Z%]*/u; +const IDENT = /^[A-Za-z_$][\w$]*[?!]?/u; + +function diffLine(line: string): CodeToken[] { + if (/^(?:diff --git|index |--- |\+\+\+ |new file|deleted file|similarity|rename (?:from|to)|old mode|new mode)/u.test(line)) return [{text: line, kind: 'meta'}]; + if (line.startsWith('@@')) return [{text: line, kind: 'meta'}]; + if (line.startsWith('+')) return [{text: line, kind: 'inserted'}]; + if (line.startsWith('-')) return [{text: line, kind: 'deleted'}]; + return [{text: line, kind: 'plain'}]; +} + +function markdownLine(line: string, state: {fence?: boolean}): CodeToken[] { + if (/^ {0,3}(?:```|~~~)/u.test(line)) { state.fence = !state.fence; return [{text: line, kind: 'meta'}]; } + if (state.fence) return [{text: line, kind: 'string'}]; + if (/^ {0,3}#{1,6}\s/u.test(line)) return [{text: line, kind: 'heading'}]; + if (/^ {0,3}>/u.test(line)) return [{text: line, kind: 'comment'}]; + const list = /^(\s*(?:[-*+]|\d+[.)])\s)(.*)$/u.exec(line); + if (list) return [{text: list[1]!, kind: 'keyword'}, {text: list[2]!, kind: 'plain'}]; + return [{text: line, kind: 'plain'}]; +} + +interface LineState { + /** Inside a block comment, the closing delimiter. */ + comment?: string; + /** Inside a multi-line string, its closing delimiter. */ + string?: string; + fence?: boolean; +} + +function tokenizeLine(line: string, grammar: Grammar, id: string, state: LineState): CodeToken[] { + const tokens: CodeToken[] = []; + const push = (text: string, kind: CodeTokenKind) => { + if (!text) return; + const last = tokens.at(-1); + if (last && last.kind === kind) last.text += text; else tokens.push({text, kind}); + }; + let index = 0; + // Keys and section headers in data formats. + if (id === 'yaml') { + const key = /^(\s*-?\s*)([\w.\-"' ]+?)(\s*:)(?=\s|$)/u.exec(line); + if (key && !line.trimStart().startsWith('#')) { push(key[1]!, 'plain'); push(key[2]!, 'property'); push(key[3]!, 'operator'); index = key[0].length; } + } else if (id === 'toml') { + if (/^\s*\[[^\]]*\]\s*$/u.test(line)) return [{text: line, kind: 'heading'}]; + const key = /^(\s*)([\w.\-"']+)(\s*=)/u.exec(line); + if (key) { push(key[1]!, 'plain'); push(key[2]!, 'property'); push(key[3]!, 'operator'); index = key[0].length; } + } else if (id === 'shell') { + // A shell session's prompt marker reads as structure, the command after it as code. + const prompt = /^(\s*[$#%❯>]\s)/u.exec(line); + if (prompt && line.trim().length > 2) { push(prompt[1]!, 'meta'); index = prompt[1]!.length; } + } + let expectCommand = id === 'shell'; + while (index < line.length) { + const rest = line.slice(index); + if (state.comment) { + const end = rest.indexOf(state.comment); + if (end < 0) { push(rest, 'comment'); return tokens; } + push(rest.slice(0, end + state.comment.length), 'comment'); index += end + state.comment.length; state.comment = undefined; continue; + } + if (state.string) { + const close = state.string; + let end = -1; + for (let at = 0; at <= rest.length - close.length; at++) { + if (rest[at] === '\\') { at++; continue; } + if (rest.startsWith(close, at)) { end = at; break; } + } + if (end < 0) { push(rest, 'string'); return tokens; } + push(rest.slice(0, end + close.length), 'string'); index += end + close.length; state.string = undefined; continue; + } + const char = line[index]!; + if (char === ' ' || char === '\t') { const space = /^\s+/u.exec(rest)![0]; push(space, 'plain'); index += space.length; continue; } + const lineComment = grammar.line?.find(marker => rest.startsWith(marker) && (marker !== '#' || id !== 'shell' || index === 0 || /\s/u.test(line[index - 1]!))); + if (lineComment && !(id === 'css')) { push(rest, 'comment'); return tokens; } + const block = grammar.block?.find(([open]) => rest.startsWith(open)); + if (block) { state.comment = block[1]; push(block[0], 'comment'); index += block[0].length; continue; } + const multi = grammar.multiline?.find(quote => rest.startsWith(quote)); + if (multi) { state.string = multi; push(multi, 'string'); index += multi.length; continue; } + if (grammar.quotes?.includes(char)) { + let end = 1; + while (end < rest.length && rest[end] !== char) end += rest[end] === '\\' ? 2 : 1; + const text = rest.slice(0, Math.min(rest.length, end + 1)); + // A JSON object key is a property, not a value. + push(text, id === 'json' && /^\s*:/u.test(rest.slice(text.length)) ? 'property' : 'string'); + index += text.length; expectCommand = false; continue; + } + if (id === 'markup' && char === '<') { + const tag = /^<\/?[A-Za-z][\w:.-]*/u.exec(rest); + if (tag) { push(tag[0], 'tag'); index += tag[0].length; continue; } + } + if (id === 'markup' && /[A-Za-z]/u.test(char)) { + const attribute = /^[A-Za-z_:][\w:.-]*(?==)/u.exec(rest); + if (attribute) { push(attribute[0], 'property'); index += attribute[0].length; continue; } + } + if (grammar.variables && char === '$') { + const variable = /^\$(?:\{[^}]{0,200}\}|[A-Za-z_]\w*|[0-9@*#?$!-])/u.exec(rest); + if (variable) { push(variable[0], 'variable'); index += variable[0].length; continue; } + } + if (grammar.decorators && char === '@') { + const decorator = /^@[A-Za-z_][\w.]*/u.exec(rest); + if (decorator) { push(decorator[0], 'meta'); index += decorator[0].length; continue; } + } + if (id === 'css' && char === '#') { + const color = /^#[0-9a-fA-F]{3,8}\b/u.exec(rest); + if (color) { push(color[0], 'constant'); index += color[0].length; continue; } + } + if (/\d/u.test(char) && !(index > 0 && /[\w$]/u.test(line[index - 1]!))) { + const number = NUMBER.exec(rest); + if (number) { push(number[0], 'number'); index += number[0].length; continue; } + } + const ident = IDENT.exec(rest); + if (ident) { + const word = ident[0]; + const after = rest.slice(word.length); + const key = grammar.caseInsensitive ? word.toLowerCase() : word; + let kind: CodeTokenKind = 'plain'; + if (id === 'css' && /^\s*:/u.test(after) && !/^\s*:[a-z-]+\s*[{,]/u.test(after)) kind = 'property'; + else if (grammar.keywords?.has(key)) kind = 'keyword'; + else if (grammar.constants?.has(key)) kind = 'constant'; + else if (expectCommand) kind = 'function'; + else if (/^\s*\(/u.test(after)) kind = 'function'; + else if (grammar.types && /^[A-Z][A-Za-z0-9]*[a-z][A-Za-z0-9]*$/u.test(word)) kind = 'type'; + else if (/^[A-Z][A-Z0-9_]{2,}$/u.test(word)) kind = 'constant'; + push(word, kind); + index += word.length; + if (id === 'shell') expectCommand = false; + continue; + } + if (id === 'shell' && (char === '|' || char === ';' || char === '&')) expectCommand = true; + push(char, /[=+\-*/%<>!&|^~?:]/u.test(char) ? 'operator' : 'plain'); + index++; + } + return tokens; +} + +/** Tokens for each line of a code block; a line's tokens always concatenate back to exactly that line. */ +export function highlightCode(lines: readonly string[], language: string | undefined): CodeToken[][] { + const id = languageId(language); + if (!id) return lines.map(line => [{text: line, kind: 'plain'}]); + const state: LineState = {}; + return lines.map((line, number) => { + if (number >= MAX_LINES || line.length > MAX_LINE_LENGTH) return [{text: line, kind: 'plain'}]; + if (id === 'diff') return diffLine(line); + if (id === 'markdown') return markdownLine(line, state); + return tokenizeLine(line, GRAMMARS[id]!, id, state); + }); +} diff --git a/src/markdown/inline.ts b/src/markdown/inline.ts new file mode 100644 index 00000000..fe8d637c --- /dev/null +++ b/src/markdown/inline.ts @@ -0,0 +1,289 @@ +import {safeHyperlinkTarget} from '../output/Hyperlinks.js'; + +/** + * Inline Markdown for untrusted text (agent replies, GitHub bodies): code spans, emphasis, strong, strikethrough, + * links, images and autolinks. The input is display-safe already (terminal controls scrubbed by the caller); this + * module only decides styling. A link target is kept only when it is a safe http(s) URL, so a hostile + * `[docs](javascript:...)` or `[x](file:///...)` renders as plain text. Parsing is linear and bounded: a span's + * delimiters never nest more than a few levels and unmatched delimiters stay literal text. + */ + +export interface InlineStyle { + code?: boolean; + strong?: boolean; + em?: boolean; + strike?: boolean; + /** A safe http(s) target; present only on link text. */ + link?: string; + /** Text of an image (its alt text): shown, never fetched. */ + image?: boolean; +} + +export interface InlineSpan { + text: string; + style: InlineStyle; +} + +const PUNCTUATION = /[!-/:-@[-`{-~\p{P}]/u; +const WHITESPACE = /\s/u; +const ALNUM = /[\p{L}\p{N}]/u; +/** Bound on scanned characters per inline run: longer paragraphs still render, only as fewer styled spans. */ +const MAX_INLINE = 64 * 1024; + +/** Untrusted text links only to the web: http(s) with a host, never file, data, javascript or custom schemes. */ +function safeLink(url: string): string | undefined { + const target = safeHyperlinkTarget(url); + return target && /^https?:/u.test(target) ? target : undefined; +} + +const ENTITIES: Record = {amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ', copy: '©', reg: '®', mdash: '—', ndash: '–', hellip: '…', rarr: '→', larr: '←', times: '×'}; + +function decodeEntity(source: string, index: number): {text: string; length: number} | undefined { + const match = /^&(?:#(\d{1,7})|#[xX]([0-9a-fA-F]{1,6})|([a-zA-Z]{2,8}));/u.exec(source.slice(index, index + 12)); + if (!match) return undefined; + if (match[3]) { const named = ENTITIES[match[3]]; return named ? {text: named, length: match[0].length} : undefined; } + const code = match[1] ? Number(match[1]) : parseInt(match[2]!, 16); + // Controls and surrogates never come back through an entity. + if (!code || code < 0x20 || (code >= 0x7f && code < 0xa0) || (code >= 0xd800 && code <= 0xdfff) || code > 0x10ffff) return undefined; + return {text: String.fromCodePoint(code), length: match[0].length}; +} + +/** Raw tokens before emphasis resolution: literal text, finished spans (code, links) and delimiter runs. */ +type Token = + | {kind: 'text'; text: string} + | {kind: 'span'; span: InlineSpan[]} + | {kind: 'delim'; char: '*' | '_' | '~'; count: number; open: boolean; close: boolean}; + +/** The text of a bracket pair starting at `start` (`[`), honoring one level of nested brackets and escapes. */ +function bracket(source: string, start: number): number { + let depth = 0; + for (let index = start; index < source.length && index < start + 2000; index++) { + const char = source[index]; + if (char === '\\') { index++; continue; } + if (char === '`') { const end = source.indexOf('`', index + 1); if (end > 0) index = end; continue; } + if (char === '[') depth++; + else if (char === ']') { depth--; if (depth === 0) return index; } + } + return -1; +} + +/** `(url "title")` after a bracket: the URL and where the destination ends. */ +function destination(source: string, open: number): {url: string; end: number} | undefined { + if (source[open] !== '(') return undefined; + let index = open + 1; + while (source[index] === ' ') index++; + let url = ''; + if (source[index] === '<') { + const close = source.indexOf('>', index); + if (close < 0) return undefined; + url = source.slice(index + 1, close); + index = close + 1; + } else { + let depth = 0; + for (; index < source.length && index < open + 4096; index++) { + const char = source[index]!; + if (char === '\\' && index + 1 < source.length) { url += source[++index]; continue; } + if (char === '(') depth++; + else if (char === ')') { if (depth === 0) break; depth--; } + else if (WHITESPACE.test(char)) break; + url += char; + } + } + while (source[index] === ' ') index++; + if (source[index] === '"' || source[index] === "'") { + const quote = source[index]!; + const close = source.indexOf(quote, index + 1); + if (close < 0) return undefined; + index = close + 1; + while (source[index] === ' ') index++; + } + if (source[index] !== ')') return undefined; + return {url, end: index + 1}; +} + +/** A trailing-punctuation-trimmed bare URL (GFM autolink literal), or undefined. */ +function bareUrl(source: string, index: number): string | undefined { + if (!/^https?:\/\//iu.test(source.slice(index, index + 8))) return undefined; + const before = source[index - 1]; + if (before && ALNUM.test(before)) return undefined; + let end = index; + while (end < source.length && end < index + 2048 && !WHITESPACE.test(source[end]!) && source[end] !== '<') end++; + let url = source.slice(index, end); + // Trailing punctuation belongs to the sentence; a closing parenthesis only when unbalanced. + for (;;) { + const last = url.at(-1); + if (!last) break; + if ('.,:;!?*_~\'"'.includes(last)) { url = url.slice(0, -1); continue; } + if (last === ')' && (url.match(/\(/gu)?.length ?? 0) < (url.match(/\)/gu)?.length ?? 0)) { url = url.slice(0, -1); continue; } + break; + } + return url.length > 8 ? url : undefined; +} + +function flanking(source: string, start: number, length: number, char: string): {open: boolean; close: boolean} { + const before = start > 0 ? source[start - 1]! : ' '; + const after = start + length < source.length ? source[start + length]! : ' '; + const beforeSpace = WHITESPACE.test(before), afterSpace = WHITESPACE.test(after); + const beforePunct = PUNCTUATION.test(before), afterPunct = PUNCTUATION.test(after); + const left = !afterSpace && (!afterPunct || beforeSpace || beforePunct); + const right = !beforeSpace && (!beforePunct || afterSpace || afterPunct); + if (char === '_') { + // Intraword underscores (snake_case) never emphasize. + return {open: left && (!right || beforePunct), close: right && (!left || afterPunct)}; + } + return {open: left, close: right}; +} + +function tokenize(source: string): Token[] { + const tokens: Token[] = []; + let text = ''; + const flush = () => { if (text) { tokens.push({kind: 'text', text}); text = ''; } }; + let index = 0; + while (index < source.length) { + const char = source[index]!; + if (char === '\\' && index + 1 < source.length && PUNCTUATION.test(source[index + 1]!)) { text += source[index + 1]; index += 2; continue; } + if (char === '&') { + const entity = decodeEntity(source, index); + if (entity) { text += entity.text; index += entity.length; continue; } + } + if (char === '`') { + let run = 1; + while (source[index + run] === '`') run++; + const fence = '`'.repeat(run); + let close = source.indexOf(fence, index + run); + // The closing run must be exactly as long as the opening one. + while (close >= 0 && source[close + run] === '`') { let skip = close; while (source[skip] === '`') skip++; close = source.indexOf(fence, skip); } + if (close >= 0) { + flush(); + let code = source.slice(index + run, close).replace(/\n/gu, ' '); + if (code.length > 2 && code.startsWith(' ') && code.endsWith(' ') && code.trim()) code = code.slice(1, -1); + tokens.push({kind: 'span', span: [{text: code, style: {code: true}}]}); + index = close + run; + continue; + } + text += fence; index += run; continue; + } + if (char === '<') { + const auto = /^<(https?:\/\/[^\s<>]{1,2000})>/iu.exec(source.slice(index, index + 2100)); + if (auto) { + flush(); + const url = safeLink(auto[1]!); + tokens.push({kind: 'span', span: [{text: auto[1]!, style: url ? {link: url} : {}}]}); + index += auto[0].length; + continue; + } + } + if (char === '!' && source[index + 1] === '[' || char === '[') { + const image = char === '!'; + const open = image ? index + 1 : index; + const close = bracket(source, open); + const target = close > 0 ? destination(source, close + 1) : undefined; + if (close > 0 && target) { + flush(); + const label = source.slice(open + 1, close); + const url = safeLink(target.url); + if (image) tokens.push({kind: 'span', span: [{text: label.trim() || 'image', style: {image: true}}]}); + else { + const inner = parseInline(label).map(span => ({text: span.text, style: {...span.style, ...(url && !span.style.code ? {link: url} : {})}})); + // A link whose target was refused still shows its text, and the refused target as text so nothing is hidden. + tokens.push({kind: 'span', span: url ? inner : [...inner, ...(target.url ? [{text: ` (${target.url.slice(0, 200)})`, style: {}}] : [])]}); + } + index = target.end; + continue; + } + } + if (char === 'h' || char === 'H') { + const url = bareUrl(source, index); + if (url) { + flush(); + const safe = safeLink(url); + tokens.push({kind: 'span', span: [{text: url, style: safe ? {link: safe} : {}}]}); + index += url.length; + continue; + } + } + if (char === '*' || char === '_' || char === '~') { + let run = 1; + while (source[index + run] === char && run < 64) run++; + if (char === '~' && run !== 2) { text += char.repeat(run); index += run; continue; } + flush(); + const {open, close} = flanking(source, index, run, char); + tokens.push({kind: 'delim', char, count: run, open, close}); + index += run; + continue; + } + text += char; index++; + } + flush(); + return tokens; +} + +/** + * Resolve emphasis with a simplified CommonMark delimiter stack: a closer pairs with the nearest compatible opener of + * the same character; two or more on both sides make strong, `~~` makes strikethrough. Unpaired delimiters become text. + */ +function resolve(tokens: Token[]): InlineSpan[] { + type Delim = {kind: 'delim'; char: '*' | '_' | '~'; count: number; length: number; open: boolean; close: boolean}; + type Node = {kind: 'text'; text: string} | {kind: 'span'; span: InlineSpan[]} | Delim; + const nodes: Node[] = tokens.map(token => token.kind === 'delim' ? {...token, length: token.count} : token); + type Wrap = {from: number; to: number; style: 'strong' | 'em' | 'strike'}; + const wraps: Wrap[] = []; + /** Positions of delimiter runs that may still open. */ + const openers: number[] = []; + for (let position = 0; position < nodes.length; position++) { + const node = nodes[position]!; + if (node.kind !== 'delim') continue; + let matched = node.close; + while (matched && node.count > 0) { + matched = false; + for (let depth = openers.length - 1; depth >= 0; depth--) { + const opener = nodes[openers[depth]!] as Delim; + if (opener.char !== node.char || opener.count === 0) continue; + if (node.char === '~' && (opener.count < 2 || node.count < 2)) continue; + // The "rule of three": runs that can both open and close do not pair when their lengths sum to a multiple of three. + if (node.char !== '~' && (opener.close || node.open) && (opener.length + node.length) % 3 === 0 && !(opener.length % 3 === 0 && node.length % 3 === 0)) continue; + const used = node.char === '~' || (opener.count >= 2 && node.count >= 2) ? 2 : 1; + wraps.push({from: openers[depth]!, to: position, style: node.char === '~' ? 'strike' : used === 2 ? 'strong' : 'em'}); + opener.count -= used; node.count -= used; + // Delimiters between the pair can no longer match; an exhausted opener leaves too. + openers.length = opener.count > 0 ? depth + 1 : depth; + matched = true; + break; + } + } + if (node.open && node.count > 0) openers.push(position); + } + const spans: InlineSpan[] = []; + const active = {strong: 0, em: 0, strike: 0}; + for (let position = 0; position < nodes.length; position++) { + // Leftover delimiter characters sit outside their pair: close before the closer, open after the opener. + for (const wrap of wraps) if (wrap.to === position) active[wrap.style]--; + const node = nodes[position]!; + const style: InlineStyle = {...(active.strong > 0 ? {strong: true} : {}), ...(active.em > 0 ? {em: true} : {}), ...(active.strike > 0 ? {strike: true} : {})}; + if (node.kind === 'text') spans.push({text: node.text, style}); + else if (node.kind === 'span') for (const span of node.span) spans.push({text: span.text, style: {...style, ...span.style}}); + else if (node.count > 0) spans.push({text: node.char.repeat(node.count), style}); + for (const wrap of wraps) if (wrap.from === position) active[wrap.style]++; + } + // Merge neighbours with identical styling so wrapping sees whole words. + const merged: InlineSpan[] = []; + for (const span of spans) { + const last = merged.at(-1); + if (last && JSON.stringify(last.style) === JSON.stringify(span.style)) last.text += span.text; + else if (span.text) merged.push({...span}); + } + return merged; +} + +/** Inline spans for one paragraph's source text. Soft line breaks become spaces; text past the bound stays plain. */ +export function parseInline(source: string): InlineSpan[] { + const bounded = source.length > MAX_INLINE ? source.slice(0, MAX_INLINE) : source; + const spans = resolve(tokenize(bounded.replace(/[ \t]*\n[ \t]*/gu, ' '))); + if (bounded.length < source.length) spans.push({text: source.slice(MAX_INLINE).replace(/\s*\n\s*/gu, ' '), style: {}}); + return spans; +} + +/** The text a reader sees, without styling (search, copy-visible, width measurement). */ +export function plainInline(source: string): string { + return parseInline(source).map(span => span.text).join(''); +} diff --git a/src/markdown/render.ts b/src/markdown/render.ts new file mode 100644 index 00000000..fe73f849 --- /dev/null +++ b/src/markdown/render.ts @@ -0,0 +1,364 @@ +import {colorEscape} from '../chroma/escape.js'; +import {colorLevel, type ColorLevel} from '../presentation/capabilities.js'; +import {getCurrentGlyphMode} from '../ui/glyphs.js'; +import {UI_COLORS, type RgbColor} from '../ui/palette.js'; +import type {SyntaxSgr} from '../input/syntaxTheme.js'; +import {displayWidth, truncateAnsi} from '../util/text.js'; +import {parseBlocks, type Alignment, type Block} from './blocks.js'; +import {highlightCode, type CodeTokenKind} from './highlight.js'; +import {parseInline, plainInline, type InlineSpan, type InlineStyle} from './inline.js'; + +/** + * Markdown for untrusted text, as terminal rows: agent replies, provider command output and GitHub bodies. Every row + * fits `columns` display cells and ends reset, so rows compose with any surrounding chrome. Colors are semantic roles + * resolved when painted (themes and NO_COLOR apply), structure never depends on color (bullets, gutters, labels), and + * Safe glyphs replace every decorative character. The caller scrubs terminal controls first (`displayText`). + */ + +export type MarkdownRowKind = 'text' | 'heading' | 'code' | 'quote' | 'table' | 'rule' | 'list' | 'blank'; + +export interface MarkdownRow { + /** Styled row, at most `columns` cells, reset at its end. */ + ansi: string; + /** What the row shows, for search, selection and copy-visible. */ + plain: string; + kind: MarkdownRowKind; + /** The code block a row belongs to (0-based, in source order), for copy-code actions. */ + code?: number; +} + +export interface MarkdownOptions { + level?: ColorLevel; + glyphs?: 'nerd' | 'safe'; + /** Emit OSC 8 hyperlinks for safe http(s) links. */ + hyperlinks?: boolean; + /** The person's syntax roles, so code follows /syntax; theme roles otherwise. */ + syntax?: SyntaxSgr; + /** Base text role for prose (agent replies use primary; quieter contexts may pass secondary). */ + tone?: 'primary' | 'secondary'; +} + +const RESET = '\u001b[0m'; +const BOLD = '\u001b[1m'; +const DIM = '\u001b[2m'; +const ITALIC = '\u001b[3m'; +const UNDERLINE = '\u001b[4m'; +const STRIKE = '\u001b[9m'; + +interface Context { + level: ColorLevel; + safe: boolean; + hyperlinks: boolean; + syntax?: SyntaxSgr; + fg: (color: RgbColor) => string; + tone: RgbColor; + codeIndex: number; +} + +function glyph(context: Context, nerd: string, safe: string): string { + return context.safe ? safe : nerd; +} + +// ---- Inline ------------------------------------------------------------------------------------------------------- + +interface Atom {text: string; sgr: string; link?: string; space: boolean} + +function spanSgr(style: InlineStyle, context: Context, base: string): string { + let sgr = base; + if (style.code) sgr = context.level === 'none' ? base : context.fg(UI_COLORS.accent); + if (style.strong) sgr += BOLD; + if (style.em) sgr += ITALIC; + if (style.strike) sgr += STRIKE; + if (style.link) sgr += `${UNDERLINE}${context.fg(UI_COLORS.accent)}`; + if (style.image) sgr += `${DIM}${context.fg(UI_COLORS.subtle)}`; + return sgr; +} + +function hostOf(url: string): string { + try { return new URL(url).host; } catch { return url; } +} + +/** Spans as wrap atoms: words and spaces, each carrying its own SGR so every row can start fresh. */ +function atomsOf(spans: readonly InlineSpan[], context: Context, base: string): Atom[] { + const atoms: Atom[] = []; + const subtle = context.fg(UI_COLORS.subtle); + for (let index = 0; index < spans.length; index++) { + const span = spans[index]!; + let text = span.text; + if (span.style.image) text = `[image: ${text}]`; + // Without color, code keeps its backticks so it never depends on color to read as code. + if (span.style.code && context.level === 'none') text = `\`${text}\``; + const sgr = spanSgr(span.style, context, base); + const link = span.style.link && context.hyperlinks ? span.style.link : undefined; + for (const part of text.split(/(\s+)/u)) { + if (!part) continue; + const space = /^\s+$/u.test(part); + atoms.push({text: space ? ' ' : part, sgr: space ? base : sgr, ...(link && !space ? {link} : {}), space}); + } + // A link reveals where it goes: its host when the text does not already show it, or its URL without hyperlinks. + const next = spans[index + 1]; + if (span.style.link && next?.style.link !== span.style.link) { + const host = hostOf(span.style.link); + const shown = span.text.includes(host) || span.text === span.style.link; + const reveal = shown ? '' : context.hyperlinks ? host : span.style.link; + if (reveal) atoms.push({text: ' ', sgr: base, space: true}, {text: `(${reveal})`, sgr: subtle, space: false}); + } + } + return atoms; +} + +function paintAtom(atom: Atom): string { + const body = atom.link ? `\u001b]8;;${atom.link}\u001b\\${atom.text}\u001b]8;;\u001b\\` : atom.text; + return `${atom.sgr}${body}${RESET}`; +} + +/** Greedy word wrap of atoms into rows of `width` cells after the given prefixes; words wider than a row hard-break. */ +function wrapAtoms(atoms: readonly Atom[], width: number, first: string, rest: string): Array<{ansi: string; plain: string}> { + const rows: Array<{ansi: string; plain: string}> = []; + let ansi = first, plain = first.replace(/\u001b\[[0-9;]*m/gu, ''), used = displayWidth(first); + let empty = true; + const push = () => { rows.push({ansi, plain: plain.trimEnd()}); ansi = rest; plain = rest.replace(/\u001b\[[0-9;]*m/gu, ''); used = displayWidth(rest); empty = true; }; + for (let index = 0; index < atoms.length; index++) { + const atom = atoms[index]!; + if (atom.space) { + // Spaces never start a row and never trail one. + if (empty) continue; + const next = atoms[index + 1]; + if (!next || used + 1 + displayWidth(next.text) > width) continue; + ansi += paintAtom(atom); plain += ' '; used += 1; + continue; + } + const size = displayWidth(atom.text); + if (!empty && used + size > width) push(); + if (used + size > width) { + let chunk = ''; + for (const char of atom.text) { + if (used + displayWidth(chunk + char) > width && chunk) { + ansi += paintAtom({...atom, text: chunk}); plain += chunk; push(); chunk = ''; + } + chunk += char; + } + ansi += paintAtom({...atom, text: chunk}); plain += chunk; used += displayWidth(chunk); empty = false; + continue; + } + ansi += paintAtom(atom); plain += atom.text; used += size; empty = false; + } + if (!empty || !rows.length) rows.push({ansi, plain: plain.trimEnd()}); + return rows; +} + +function inlineRows(source: string, width: number, context: Context, base: string, first = '', rest = first): Array<{ansi: string; plain: string}> { + // A hard break inside a paragraph is a new row with the same hanging indent. + return source.split('\n').flatMap((line, index) => wrapAtoms(atomsOf(parseInline(line), context, base), width, index === 0 ? first : rest, rest)); +} + +// ---- Code --------------------------------------------------------------------------------------------------------- + +function tokenSgr(kind: CodeTokenKind, context: Context): string { + const s = context.syntax; + const fg = context.fg; + switch (kind) { + case 'keyword': case 'tag': return s?.KnownCommand ?? fg(UI_COLORS.accent); + case 'function': return s?.Function ?? fg(UI_COLORS.accent); + case 'string': return s?.String ?? fg(UI_COLORS.secondary); + case 'number': case 'constant': case 'variable': return s?.Variable ?? fg(UI_COLORS.accent); + case 'comment': return `${ITALIC}${s?.Comment ?? fg(UI_COLORS.subtle)}`; + case 'type': case 'property': return s?.Path ?? fg(UI_COLORS.secondary); + case 'operator': return s?.Operator ?? fg(UI_COLORS.subtle); + case 'inserted': return fg(UI_COLORS.success); + case 'deleted': return fg(UI_COLORS.failure); + case 'meta': return fg(UI_COLORS.subtle); + case 'heading': return `${BOLD}${fg(UI_COLORS.primary)}`; + case 'plain': return s?.Normal ?? fg(UI_COLORS.primary); + } +} + +function codeRows(block: Extract, width: number, context: Context): MarkdownRow[] { + const index = context.codeIndex++; + const gutter = `${context.fg(UI_COLORS.separator)}${glyph(context, '│', '|')}${RESET} `; + const inner = Math.max(1, width - 2); + const label = block.language ? block.language.slice(0, 24) : ''; + const rows: MarkdownRow[] = []; + const tokens = highlightCode(block.lines.length ? block.lines : [''], block.language); + tokens.forEach((line, number) => { + // Wrap long lines by cells; continuation rows indent two cells so a wrap reads as one line. + let ansi = '', plain = '', used = 0; + const flush = () => { rows.push({ansi: `${gutter}${ansi}${RESET}`, plain: `${glyph(context, '│', '|')} ${plain}`.trimEnd(), kind: 'code', code: index}); ansi = ' '; plain = ' '; used = 2; }; + for (const token of line) { + const sgr = tokenSgr(token.kind, context); + let text = ''; + for (const char of token.text) { + const size = displayWidth(char); + if (used + size > inner) { if (text) ansi += `${sgr}${text}${RESET}`; plain += text; text = ''; flush(); } + text += char; used += size; + } + if (text) { ansi += `${sgr}${text}${RESET}`; plain += text; } + } + rows.push({ansi: `${gutter}${ansi}${RESET}`, plain: `${glyph(context, '│', '|')} ${plain}`.trimEnd(), kind: 'code', code: index}); + if (number === 0 && label) { + // The language rides the first row's right edge when there is room, never displacing code. + const first = rows[0]!; + const free = width - displayWidth(first.ansi); + if (free >= label.length + 2) first.ansi = `${first.ansi}${' '.repeat(free - label.length)}${context.fg(UI_COLORS.subtle)}${label}${RESET}`; + } + }); + if (!block.closed) rows.push({ansi: `${gutter}${context.fg(UI_COLORS.subtle)}${glyph(context, '…', '...')}${RESET}`, plain: `${glyph(context, '│', '|')} ${glyph(context, '…', '...')}`, kind: 'code', code: index}); + return rows; +} + +// ---- Tables ------------------------------------------------------------------------------------------------------- + +function alignCell(text: string, width: number, align: Alignment): string { + const free = Math.max(0, width - displayWidth(text)); + if (align === 'right') return `${' '.repeat(free)}${text}`; + if (align === 'center') return `${' '.repeat(Math.floor(free / 2))}${text}${' '.repeat(Math.ceil(free / 2))}`; + return `${text}${' '.repeat(free)}`; +} + +function tableRows(block: Extract, width: number, context: Context): MarkdownRow[] { + const columns = block.header.length; + const all = [block.header, ...block.rows]; + const natural = Array.from({length: columns}, (_, column) => Math.max(1, ...all.map(row => displayWidth(plainInline(row[column] ?? ''))))); + const separator = glyph(context, ' │ ', ' | '); + const chrome = 3 * (columns - 1); + const available = width - chrome; + const minimum = Array.from({length: columns}, (_, column) => Math.min(natural[column]!, Math.max(3, Math.min(10, ...[block.header[column] ?? ''].map(cell => displayWidth(plainInline(cell))))))); + if (available < minimum.reduce((a, b) => a + b, 0)) { + // Too narrow for a grid: each row becomes a short record of "Header: value" lines. + const rows: MarkdownRow[] = []; + const subtle = context.fg(UI_COLORS.subtle), base = context.fg(context.tone); + block.rows.forEach((row, index) => { + if (index) rows.push({ansi: '', plain: '', kind: 'blank'}); + row.forEach((cell, column) => { + const label = plainInline(block.header[column] ?? ''); + const lead = `${subtle}${label}:${RESET} `; + for (const line of inlineRows(cell || '—', width, context, base, lead, ' ')) rows.push({...line, kind: 'table'}); + }); + }); + return rows; + } + // Shrink the widest columns first until the grid fits, wrapping their cells. + const widths = [...natural]; + while (widths.reduce((a, b) => a + b, 0) > available) { + const widest = widths.indexOf(Math.max(...widths.map((value, column) => value > minimum[column]! ? value : 0))); + if (widest < 0 || widths[widest]! <= minimum[widest]!) break; + widths[widest]! -= 1; + } + const rule = context.fg(UI_COLORS.separator); + const out: MarkdownRow[] = []; + const renderRow = (cells: readonly string[], header: boolean) => { + const base = header ? `${BOLD}${context.fg(UI_COLORS.primary)}` : context.fg(context.tone); + const wrapped = cells.map((cell, column) => inlineRows(cell, widths[column]!, context, base)); + const height = Math.max(...wrapped.map(lines => lines.length)); + for (let line = 0; line < height; line++) { + const ansi = wrapped.map((lines, column) => { + const part = lines[line]; + const text = part ? alignCell(part.ansi, widths[column]!, block.align[column]) : ' '.repeat(widths[column]!); + return text; + }).join(`${rule}${separator}${RESET}`); + const plain = wrapped.map((lines, column) => alignCell(lines[line]?.plain ?? '', widths[column]!, block.align[column])).join(separator); + out.push({ansi, plain: plain.trimEnd(), kind: 'table'}); + } + }; + renderRow(block.header, true); + const line = widths.map(size => glyph(context, '─', '-').repeat(size)).join(glyph(context, '─┼─', '-+-')); + out.push({ansi: `${rule}${line}${RESET}`, plain: line, kind: 'table'}); + for (const row of block.rows) renderRow(row, false); + return out; +} + +// ---- Blocks ------------------------------------------------------------------------------------------------------- + +const BULLETS = {nerd: ['•', '◦', '▪'], safe: ['-', '*', '+']}; + +function renderBlocks(blocks: readonly Block[], width: number, context: Context, depth: number, tight = false): MarkdownRow[] { + const out: MarkdownRow[] = []; + const gap = () => { if (out.length && out.at(-1)!.kind !== 'blank') out.push({ansi: '', plain: '', kind: 'blank'}); }; + const base = context.fg(context.tone); + for (const block of blocks) { + if (!tight) gap(); + switch (block.kind) { + case 'paragraph': + for (const row of inlineRows(block.text, width, context, base)) out.push({...row, kind: 'text'}); + break; + case 'heading': { + const role = block.level === 1 ? UI_COLORS.accent : block.level === 2 ? UI_COLORS.primary : UI_COLORS.secondary; + const sgr = `${block.level <= 3 ? BOLD : ''}${block.level >= 4 ? ITALIC : ''}${context.fg(role)}`; + for (const row of inlineRows(block.text, width, context, sgr)) out.push({...row, kind: 'heading'}); + break; + } + case 'code': + out.push(...codeRows(block, width, context)); + break; + case 'rule': { + const line = glyph(context, '─', '-').repeat(Math.max(1, Math.min(width, 48))); + out.push({ansi: `${context.fg(UI_COLORS.separator)}${line}${RESET}`, plain: line, kind: 'rule'}); + break; + } + case 'quote': { + const bar = `${context.fg(UI_COLORS.subtle)}${glyph(context, '▎', '|')}${RESET} `; + const inner = renderBlocks(block.blocks, Math.max(1, width - 2), {...context, tone: UI_COLORS.secondary}, depth + 1); + for (const row of inner) out.push({ansi: `${bar}${row.ansi}`, plain: `${glyph(context, '▎', '|')} ${row.plain}`.trimEnd(), kind: 'quote', ...(row.code !== undefined ? {code: row.code} : {})}); + break; + } + case 'table': + out.push(...tableRows(block, width, context)); + break; + case 'list': { + const numberWidth = block.ordered ? String(block.start + block.items.length - 1).length + 1 : 0; + block.items.forEach((item, index) => { + if (block.loose && index) out.push({ansi: '', plain: '', kind: 'blank'}); + const subtle = context.fg(UI_COLORS.subtle); + let marker: string; + if (item.task) marker = item.task === 'done' ? glyph(context, '☑', '[x]') : glyph(context, '☐', '[ ]'); + else if (block.ordered) marker = `${item.number ?? block.start + index}.`.padStart(numberWidth); + else marker = (context.safe ? BULLETS.safe : BULLETS.nerd)[depth % 3]!; + const lead = displayWidth(marker) + 1; + const markerSgr = item.task === 'done' ? context.fg(UI_COLORS.success) : block.ordered ? subtle : context.fg(UI_COLORS.accent); + const childContext = item.task === 'done' ? {...context, tone: UI_COLORS.subtle} : context; + const inner = renderBlocks(item.blocks, Math.max(1, width - lead), childContext, depth + 1, !block.loose); + if (!inner.length) inner.push({ansi: '', plain: '', kind: 'text'}); + inner.forEach((row, line) => { + const prefix = line === 0 ? `${markerSgr}${marker}${RESET} ` : ' '.repeat(lead); + out.push({ansi: `${prefix}${row.ansi}`, plain: `${line === 0 ? `${marker} ` : ' '.repeat(lead)}${row.plain}`.trimEnd(), kind: row.kind === 'blank' ? 'blank' : 'list', ...(row.code !== undefined ? {code: row.code} : {})}); + }); + }); + break; + } + } + } + return out; +} + +/** Rows for untrusted Markdown at `columns` cells. */ +export function renderMarkdown(source: string, columns: number, options: MarkdownOptions = {}): MarkdownRow[] { + const width = Math.max(4, columns); + const level = options.level ?? colorLevel(); + const context: Context = { + level, + safe: (options.glyphs ?? getCurrentGlyphMode()) === 'safe', + hyperlinks: options.hyperlinks ?? false, + ...(options.syntax ? {syntax: options.syntax} : {}), + fg: color => colorEscape(38, color, level), + tone: options.tone === 'secondary' ? UI_COLORS.secondary : UI_COLORS.primary, + codeIndex: 0, + }; + const rows = renderBlocks(parseBlocks(source), width, context, 0); + while (rows.length && rows.at(-1)!.kind === 'blank') rows.pop(); + // A last line of defence: no row may exceed the width whatever a rule above computed. + return rows.map(row => displayWidth(row.ansi) > width ? {...row, ansi: truncateAnsi(row.ansi, width)} : row); +} + +/** The code blocks of a Markdown text, in order: what a copy-code action copies (exact source lines). */ +export function codeBlocks(source: string): Array<{language?: string; text: string}> { + const found: Array<{language?: string; text: string}> = []; + const walk = (blocks: readonly Block[]) => { + for (const block of blocks) { + if (block.kind === 'code') found.push({...(block.language ? {language: block.language} : {}), text: block.lines.join('\n')}); + else if (block.kind === 'quote') walk(block.blocks); + else if (block.kind === 'list') for (const item of block.items) walk(item.blocks); + } + }; + walk(parseBlocks(source)); + return found; +} diff --git a/tests/markdownRender.test.ts b/tests/markdownRender.test.ts new file mode 100644 index 00000000..3368e291 --- /dev/null +++ b/tests/markdownRender.test.ts @@ -0,0 +1,180 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {parseInline, plainInline} from '../src/markdown/inline.js'; +import {parseBlocks} from '../src/markdown/blocks.js'; +import {highlightCode} from '../src/markdown/highlight.js'; +import {codeBlocks, renderMarkdown} from '../src/markdown/render.js'; +import {displayWidth, stripAnsi} from '../src/util/text.js'; + +const styleOf = (source: string, text: string) => parseInline(source).find(span => span.text.includes(text))?.style; +const plain = (source: string, columns = 60, options = {}) => renderMarkdown(source, columns, {level: 'truecolor', glyphs: 'nerd', ...options}).map(row => row.plain); + +test('inline: emphasis, strong, strikethrough and code spans', () => { + assert.deepEqual(styleOf('a *b* c', 'b'), {em: true}); + assert.deepEqual(styleOf('a **b** c', 'b'), {strong: true}); + assert.deepEqual(styleOf('a ***b*** c', 'b'), {strong: true, em: true}); + assert.deepEqual(styleOf('a ~~gone~~ c', 'gone'), {strike: true}); + assert.deepEqual(styleOf('use `npm test` now', 'npm test'), {code: true}); + assert.equal(plainInline('``a ` b``'), 'a ` b', 'a longer backtick run may contain a shorter one'); + assert.equal(plainInline('**bold *nested* bold**'), 'bold nested bold'); + assert.deepEqual(styleOf('**bold *nested* bold**', 'nested'), {strong: true, em: true}); +}); + +test('inline: identifiers and arithmetic stay literal where emphasis would be wrong', () => { + assert.equal(plainInline('call my_function_name and a_b_c here'), 'call my_function_name and a_b_c here'); + // CommonMark (and GitHub) make __init__ strong; replies normally put such names in code spans. + assert.deepEqual(styleOf('the __init__ method', 'init'), {strong: true}); + assert.equal(plainInline('a * b * c'), 'a * b * c', 'spaced asterisks are not emphasis'); + assert.equal(plainInline('**unclosed bold'), '**unclosed bold'); + assert.equal(plainInline('\\*literal\\*'), '*literal*'); + assert.equal(plainInline('`code with **stars**`'), 'code with **stars**'); +}); + +test('inline: only safe http(s) links become links; refused targets stay visible as text', () => { + assert.equal(styleOf('[docs](https://example.com/a)', 'docs')?.link, 'https://example.com/a'); + const hostile = parseInline('[click](javascript:alert(1))'); + assert.ok(hostile.every(span => !span.style.link)); + assert.ok(hostile.map(span => span.text).join('').includes('javascript:alert(1)'), 'the refused target is shown, never hidden behind text'); + assert.ok(parseInline('[f](file:///etc/passwd)').every(span => !span.style.link)); + assert.equal(styleOf('', 'example')?.link, 'https://example.com/'); + assert.equal(plainInline('see https://example.com/docs.'), 'see https://example.com/docs.'); + assert.equal(styleOf('see https://example.com/docs.', 'example')?.link, 'https://example.com/docs'); + assert.equal(plainInline('![a diagram](https://x.y/z.png)'), 'a diagram'); + assert.equal(plainInline('Tom & Jerry <3 A'), 'Tom & Jerry <3 A'); + assert.equal(plainInline(''), '', 'entities never produce control characters'); +}); + +test('blocks: headings, paragraphs, rules and hard breaks', () => { + const blocks = parseBlocks('# Title\n\nSetext\n======\n\nline one \nline two\n\n---\n\n## Sub ##'); + assert.deepEqual(blocks.map(block => block.kind), ['heading', 'heading', 'paragraph', 'rule', 'heading']); + assert.deepEqual(blocks[0], {kind: 'heading', level: 1, text: 'Title'}); + assert.deepEqual(blocks[1], {kind: 'heading', level: 1, text: 'Setext'}); + assert.equal((blocks[2] as {text: string}).text, 'line one\nline two'); + assert.deepEqual(blocks[4], {kind: 'heading', level: 2, text: 'Sub'}); +}); + +test('blocks: fenced code keeps lines literally; an unclosed fence runs to the end (streaming)', () => { + const closed = parseBlocks('```ts\nconst a = 1;\n indented();\n```\nafter'); + assert.deepEqual(closed[0], {kind: 'code', language: 'ts', lines: ['const a = 1;', ' indented();'], closed: true}); + assert.deepEqual(closed[1], {kind: 'paragraph', text: 'after'}); + const open = parseBlocks('text\n\n~~~python\nprint("hi")\n# still code'); + assert.deepEqual(open[1], {kind: 'code', language: 'python', lines: ['print("hi")', '# still code'], closed: false}); + assert.deepEqual(parseBlocks('````\n```\ninner\n```\n````')[0], {kind: 'code', lines: ['```', 'inner', '```'], closed: true}); +}); + +test('blocks: nested and ordered lists, task items, loose lists and quotes', () => { + const [list] = parseBlocks('3. three\n4. four\n - nested\n - [x] done\n5. five'); + assert.equal(list!.kind, 'list'); + const ordered = list as Extract; + assert.equal(ordered.ordered, true); + assert.equal(ordered.start, 3); + assert.equal(ordered.items.length, 3); + const nested = ordered.items[1]!.blocks[1] as Extract; + assert.equal(nested.kind, 'list'); + assert.equal(nested.items[1]!.task, 'done'); + assert.equal((parseBlocks('- a\n\n- b')[0] as {loose: boolean}).loose, true); + assert.equal((parseBlocks('- a\n- b')[0] as {loose: boolean}).loose, false); + const [quote] = parseBlocks('> quoted\nlazy line\n> - item'); + assert.equal(quote!.kind, 'quote'); + assert.deepEqual((quote as {blocks: Array<{kind: string}>}).blocks.map(block => block.kind), ['paragraph', 'list']); + assert.equal(parseBlocks('* * *')[0]!.kind, 'rule', 'a thematic break wins over a list item'); +}); + +test('blocks: GFM tables with alignment and escaped pipes', () => { + const [table] = parseBlocks('| a | b | c |\n|:--|:-:|--:|\n| 1 | `x|y` | 3 \\| 4 |\n| short |'); + assert.equal(table!.kind, 'table'); + const t = table as Extract; + assert.deepEqual(t.align, ['left', 'center', 'right']); + assert.deepEqual(t.rows[0], ['1', '`x|y`', '3 | 4']); + assert.deepEqual(t.rows[1], ['short', '', ''], 'short rows are padded to the header width'); +}); + +test('render: every row fits its width at every width, for every construct', () => { + const source = [ + '# A very long heading that certainly needs wrapping at narrow widths', + 'Paragraph with **bold**, *em*, `code`, a [link](https://example.com/very/long/path?q=1) and averyveryveryverylongunbrokenword.', + '- item one that wraps\n - nested item\n 1. deep ordered', + '> quote with a list\n> - inside', + '```js\nconst x = "a long string literal that will wrap at narrow widths"; // comment\n```', + '| Column | Another column | Third |\n|---|---|---|\n| value with several words | 2 | three |', + '---', + ].join('\n\n'); + for (const columns of [4, 8, 12, 20, 30, 40, 60, 80, 120, 200]) { + for (const glyphs of ['nerd', 'safe'] as const) { + for (const row of renderMarkdown(source, columns, {level: 'truecolor', glyphs, hyperlinks: true})) { + assert.ok(displayWidth(row.ansi) <= Math.max(4, columns), `${columns}/${glyphs}: "${stripAnsi(row.ansi)}" is ${displayWidth(row.ansi)} wide`); + const sgr = row.ansi.match(/\u001b\[[0-9;]*m/gu) ?? []; + assert.ok(!sgr.length || sgr.at(-1) === '\u001b[0m', `no style leaks past the row: ${JSON.stringify(row.ansi)}`); + assert.equal((row.ansi.match(/\u001b\]8;;[^\u001b]/gu) ?? []).length, (row.ansi.match(/\u001b\]8;;\u001b/gu) ?? []).length, 'every hyperlink closes on its row'); + } + } + } +}); + +test('render: tables fit by wrapping cells, then fall back to records when far too narrow', () => { + const table = '| Name | Description |\n|---|---|\n| sum | adds two numbers together and returns the result |'; + const wide = plain(table, 80); + assert.equal(wide[0], 'Name │ Description'); + assert.ok(wide[1]!.startsWith('─')); + const narrow = plain(table, 30); + assert.ok(narrow.length > 3, 'cells wrap into more rows'); + assert.ok(narrow.every(row => displayWidth(row) <= 30)); + const tiny = plain('| A | B | C | D | E |\n|---|---|---|---|---|\n| 1 | 2 | 3 | 4 | 5 |', 12); + assert.ok(tiny.includes('A: 1') && tiny.includes('E: 5'), `records: ${JSON.stringify(tiny)}`); +}); + +test('render: Safe glyphs and NO_COLOR keep structure without decorative characters or color', () => { + const source = '- a\n - b\n- [x] c\n\n> q\n\n```\ncode\n```\n\n| x | y |\n|---|---|\n| 1 | 2 |\n\nUse `npm test`.'; + const rows = renderMarkdown(source, 40, {level: 'none', glyphs: 'safe'}); + const text = rows.map(row => row.plain).join('\n'); + assert.ok(!/[•◦▪☑☐▎│─┼]/u.test(text), `no Unicode decoration in Safe mode:\n${text}`); + assert.ok(text.includes('- a') && text.includes('[x] c') && text.includes('| q') && text.includes('| code') && text.includes('x | y')); + assert.ok(text.includes('Use `npm test`.'), 'without color, inline code keeps its backticks'); + assert.ok(rows.every(row => !/\u001b\[(?:3|4)8;/u.test(row.ansi)), 'no color escapes without color'); +}); + +test('render: OSC 8 hyperlinks only for safe links, and the destination host is always visible', () => { + const rows = renderMarkdown('[docs](https://example.com/a) and [bad](javascript:x)', 80, {level: 'truecolor', hyperlinks: true}); + const ansi = rows.map(row => row.ansi).join(''); + assert.ok(ansi.includes('\u001b]8;;https://example.com/a\u001b\\docs'), 'safe link is a hyperlink'); + assert.ok(!ansi.includes(']8;;javascript'), 'unsafe target is never a hyperlink'); + assert.ok(rows[0]!.plain.includes('docs (example.com)'), 'the host is shown beside link text'); + const offline = renderMarkdown('[docs](https://example.com/a)', 80, {level: 'truecolor', hyperlinks: false}); + assert.ok(offline[0]!.plain.includes('docs (https://example.com/a)'), 'without hyperlinks the full URL is visible'); +}); + +test('render: code is highlighted without changing its text; copy-code returns exact source lines', () => { + const source = 'Intro\n\n```ts\nconst x = 1; // one\n```\n\n- ```sh\n npm test\n ```'; + const rows = renderMarkdown(source, 60, {level: 'truecolor'}); + const code = rows.filter(row => row.kind === 'code' || row.code !== undefined); + assert.ok(code.some(row => row.plain.includes('const x = 1; // one'))); + assert.deepEqual(codeBlocks(source), [{language: 'ts', text: 'const x = 1; // one'}, {language: 'sh', text: 'npm test'}]); + for (const language of ['ts', 'python', 'rust', 'go', 'sh', 'json', 'yaml', 'toml', 'html', 'css', 'sql', 'diff', 'markdown', 'unknown-lang']) { + const lines = ['const a = "x"; /* c */ # z', ' call(1, 2.5) -- y', '+added', '
t
']; + const tokens = highlightCode(lines, language); + tokens.forEach((line, index) => assert.equal(line.map(token => token.text).join(''), lines[index], `${language}: tokens rebuild the line`)); + } + const diff = highlightCode(['@@ -1 +1 @@', '-old', '+new', ' same'], 'diff'); + assert.deepEqual(diff.map(line => line[0]!.kind), ['meta', 'deleted', 'inserted', 'plain']); +}); + +test('highlight: block comments and multi-line strings carry across lines', () => { + const tokens = highlightCode(['a /* start', 'still comment', 'end */ b', 'x = """doc', 'more"""'], 'python'); + assert.equal(tokens[1]![0]!.kind, 'plain', 'Python has no /* */ comments'); + const js = highlightCode(['a /* start', 'still comment', 'end */ b', 'const t = `tpl', 'line ${x}`;'], 'ts'); + assert.equal(js[1]![0]!.kind, 'comment'); + assert.equal(js[2]![0]!.kind, 'comment'); + assert.equal(js[4]![0]!.kind, 'string'); + const py = highlightCode(['x = """doc', 'more""" + y'], 'python'); + assert.equal(py[1]![0]!.kind, 'string'); +}); + +test('render: deep nesting and huge inputs stay bounded', () => { + const deep = `${'> '.repeat(40)}deep`; + assert.ok(plain(deep, 80).join('').includes('deep')); + const big = Array.from({length: 3000}, (_, index) => `- item ${index}`).join('\n'); + const started = Date.now(); + const rows = renderMarkdown(big, 80, {level: 'truecolor'}); + assert.equal(rows.length, 3000); + assert.ok(Date.now() - started < 2000, 'thousands of rows render quickly'); +}); From 45ad1ebef8b2d663701996337a6b77e710aacceb Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Fri, 9 Oct 2026 08:53:01 +0530 Subject: [PATCH 003/249] Markdown: reveal real link destinations, refuse smuggled characters, keep parsing linear A security review of the untrusted-Markdown renderer found three problems, fixed here: - Link text could hide a different destination when the target's host was only a substring of the text (example.com -> le.com). Links are now judged on their whole text: no destination is revealed only when the text names exactly the target's host as a whole hostname and no other; internationalized (punycode) hosts are always shown. - Numeric entities decoded after the caller's scrubber, so ‮ and similar could reintroduce bidi overrides or invisible characters. Entities now refuse controls, format characters, separators and private use, and the renderer scrubs its own input instead of trusting callers. - Several recognizers backtracked quadratically on long lines (heading, fence, rule, hard and soft breaks, YAML keys, code spans, emphasis, link brackets, long-word breaking). They are linear scans or bounded searches now, with timing tests on pathological inputs. --- src/markdown/blocks.ts | 90 +++++++++++++++++++++++++++++------- src/markdown/highlight.ts | 9 +++- src/markdown/inline.ts | 75 +++++++++++++++++++++++++----- src/markdown/render.ts | 60 +++++++++++++++++++----- tests/markdownRender.test.ts | 42 +++++++++++++++++ 5 files changed, 235 insertions(+), 41 deletions(-) diff --git a/src/markdown/blocks.ts b/src/markdown/blocks.ts index d3164959..6ed72200 100644 --- a/src/markdown/blocks.ts +++ b/src/markdown/blocks.ts @@ -27,9 +27,6 @@ export interface ListItem { const MAX_DEPTH = 12; const MAX_LINES = 50_000; -const FENCE = /^( {0,3})(`{3,}|~{3,})[ \t]*([^`]*?)[ \t]*$/u; -const HEADING = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?(?:[ \t]+#+)?[ \t]*$/u; -const RULE = /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/u; const QUOTE = /^ {0,3}>[ ]?/u; const ITEM = /^( {0,3})([-+*]|\d{1,9}[.)])([ \t]+|$)/u; const SETEXT = /^ {0,3}(=+|-+)[ \t]*$/u; @@ -38,6 +35,70 @@ const DELIMITER_CELL = /^:?-+:?$/u; const blank = (line: string) => line.trim() === ''; const indentOf = (line: string) => line.length - line.trimStart().length; +/* + * Block markers are recognized by linear scans rather than backtracking patterns: agent text is untrusted and a + * single reply line may be tens of kilobytes, so no recognizer may be worse than linear in the line's length. + */ + +/** Opening code fence: up to three spaces, three or more backticks or tildes, then an info string. */ +function fenceOf(line: string): {indent: number; marker: string; info: string} | undefined { + const indent = indentOf(line); + if (indent > 3) return undefined; + const char = line[indent]; + if (char !== '`' && char !== '~') return undefined; + let end = indent; + while (line[end] === char) end++; + if (end - indent < 3) return undefined; + const info = line.slice(end).trim(); + // A backtick fence's info string cannot contain a backtick (that is inline code instead). + if (char === '`' && info.includes('`')) return undefined; + return {indent, marker: line.slice(indent, end), info}; +} + +/** A closing fence for `marker`: the same character, at least as long, nothing else on the line. */ +function closesFence(line: string, marker: string): boolean { + const indent = indentOf(line); + if (indent > 3) return false; + let end = indent; + while (line[end] === marker[0]) end++; + return end - indent >= marker.length && line.slice(end).trim() === ''; +} + +/** ATX heading: level and text with any closing `#` sequence removed. */ +function headingOf(line: string): {level: number; text: string} | undefined { + const indent = indentOf(line); + if (indent > 3 || line[indent] !== '#') return undefined; + let end = indent; + while (line[end] === '#') end++; + const level = end - indent; + if (level > 6 || (end < line.length && line[end] !== ' ' && line[end] !== '\t')) return undefined; + let text = line.slice(end).trim(); + // A closing sequence is a run of # preceded by a space (or the whole content). + let cut = text.length; + while (cut > 0 && text[cut - 1] === '#') cut--; + if (cut === 0) text = ''; + else if (cut < text.length && (text[cut - 1] === ' ' || text[cut - 1] === '\t')) text = text.slice(0, cut).trimEnd(); + return {level, text}; +} + +/** Thematic break: three or more of one of - * _, optionally spaced, nothing else. */ +function isRule(line: string): boolean { + const indent = indentOf(line); + if (indent > 3) return false; + const char = line[indent]; + if (char !== '-' && char !== '*' && char !== '_') return false; + let count = 0; + for (let index = indent; index < line.length; index++) { + const current = line[index]; + if (current === char) count++; + else if (current !== ' ' && current !== '\t') return false; + } + return count >= 3; +} + +/** A hard line break at the end of a paragraph line: two trailing spaces or a backslash. */ +const hardBreak = (line: string) => line.endsWith(' ') || line.endsWith('\\'); + /** Cells of a pipe table row; escaped pipes stay text, and pipes inside code spans do not split. */ export function splitRow(line: string): string[] { let text = line.trim(); @@ -65,7 +126,7 @@ function delimiterRow(line: string): Alignment[] | undefined { /** Whether a line starts a block that interrupts a paragraph (a lazy continuation line must not). */ function interrupts(line: string, next: string | undefined): boolean { - if (FENCE.test(line) || HEADING.test(line) || RULE.test(line) || QUOTE.test(line)) return true; + if (fenceOf(line) || headingOf(line) || isRule(line) || QUOTE.test(line)) return true; const item = ITEM.exec(line); // An ordered item interrupts a paragraph only when it starts at 1, and an empty item never does (CommonMark). if (item && line.slice(item[0].length).trim() && (!/\d/u.test(item[2]!) || /^1[.)]$/u.test(item[2]!))) return true; @@ -89,29 +150,26 @@ function parseLines(lines: readonly string[], depth: number): Block[] { const line = lines[index]!; if (blank(line)) { index++; continue; } - const fence = FENCE.exec(line); - if (fence && !(fence[2]!.startsWith('`') && fence[3]!.includes('`'))) { - const marker = fence[2]!; - const indent = fence[1]!.length; + const fence = fenceOf(line); + if (fence) { const code: string[] = []; let closed = false; for (index++; index < lines.length; index++) { const candidate = lines[index]!; - const close = /^ {0,3}(`{3,}|~{3,})[ \t]*$/u.exec(candidate); - if (close && close[1]![0] === marker[0] && close[1]!.length >= marker.length) { closed = true; index++; break; } + if (closesFence(candidate, fence.marker)) { closed = true; index++; break; } // Content keeps its own indentation, less the fence's. - code.push(candidate.slice(Math.min(indent, indentOf(candidate)))); + code.push(candidate.slice(Math.min(fence.indent, indentOf(candidate)))); } - const language = fence[3]!.split(/[\s{,]/u)[0]?.toLowerCase() || undefined; + const language = fence.info.split(/[\s{,]/u)[0]?.toLowerCase().slice(0, 40) || undefined; blocks.push({kind: 'code', ...(language ? {language} : {}), lines: code, closed}); continue; } - const heading = HEADING.exec(line); - if (heading) { blocks.push({kind: 'heading', level: heading[1]!.length, text: (heading[2] ?? '').trim()}); index++; continue; } + const heading = headingOf(line); + if (heading) { blocks.push({kind: 'heading', level: heading.level, text: heading.text}); index++; continue; } // A thematic break wins over a list item (`* * *`). - if (RULE.test(line)) { blocks.push({kind: 'rule'}); index++; continue; } + if (isRule(line)) { blocks.push({kind: 'rule'}); index++; continue; } if (QUOTE.test(line)) { const inner: string[] = []; @@ -172,7 +230,7 @@ function parseLines(lines: readonly string[], depth: number): Block[] { if (interrupts(candidate, lines[index + 1])) break; // A hard line break (two trailing spaces or a backslash) keeps the break. const previous = paragraph.at(-1)!; - if (/ {2,}$|\\$/u.test(lines[index - 1]!)) paragraph[paragraph.length - 1] = `${previous.replace(/\\$/u, '')}\n`; + if (hardBreak(lines[index - 1]!)) paragraph[paragraph.length - 1] = `${previous.endsWith('\\') ? previous.slice(0, -1) : previous}\n`; paragraph.push(candidate.trim()); } if (paragraph.length) blocks.push({kind: 'paragraph', text: paragraph.join('\n').replace(/\n\n/gu, '\n')}); diff --git a/src/markdown/highlight.ts b/src/markdown/highlight.ts index 5b41c95d..a7cfd176 100644 --- a/src/markdown/highlight.ts +++ b/src/markdown/highlight.ts @@ -130,8 +130,13 @@ function tokenizeLine(line: string, grammar: Grammar, id: string, state: LineSta let index = 0; // Keys and section headers in data formats. if (id === 'yaml') { - const key = /^(\s*-?\s*)([\w.\-"' ]+?)(\s*:)(?=\s|$)/u.exec(line); - if (key && !line.trimStart().startsWith('#')) { push(key[1]!, 'plain'); push(key[2]!, 'property'); push(key[3]!, 'operator'); index = key[0].length; } + // `key:` (optionally after a list dash), found by a linear scan to the first colon that ends the key. + const lead = /^\s*(?:-\s+)?/u.exec(line)![0]; + const colon = line.indexOf(':', lead.length); + const key = colon > lead.length ? line.slice(lead.length, colon) : ''; + if (key.trim() && /^[\w.\-"' ]+$/u.test(key) && (colon + 1 === line.length || line[colon + 1] === ' ') && !line.trimStart().startsWith('#')) { + push(lead, 'plain'); push(key, 'property'); push(':', 'operator'); index = colon + 1; + } } else if (id === 'toml') { if (/^\s*\[[^\]]*\]\s*$/u.test(line)) return [{text: line, kind: 'heading'}]; const key = /^(\s*)([\w.\-"']+)(\s*=)/u.exec(line); diff --git a/src/markdown/inline.ts b/src/markdown/inline.ts index fe8d637c..1a1c1407 100644 --- a/src/markdown/inline.ts +++ b/src/markdown/inline.ts @@ -27,6 +27,8 @@ export interface InlineSpan { const PUNCTUATION = /[!-/:-@[-`{-~\p{P}]/u; const WHITESPACE = /\s/u; const ALNUM = /[\p{L}\p{N}]/u; +/** How far back a closing delimiter looks for its opener. */ +const MAX_OPENERS = 64; /** Bound on scanned characters per inline run: longer paragraphs still render, only as fewer styled spans. */ const MAX_INLINE = 64 * 1024; @@ -36,6 +38,8 @@ function safeLink(url: string): string | undefined { return target && /^https?:/u.test(target) ? target : undefined; } +const INVISIBLE = /[\p{Cc}\p{Cf}\p{Co}\p{Zl}\p{Zp}]/u; + const ENTITIES: Record = {amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ', copy: '©', reg: '®', mdash: '—', ndash: '–', hellip: '…', rarr: '→', larr: '←', times: '×'}; function decodeEntity(source: string, index: number): {text: string; length: number} | undefined { @@ -43,9 +47,12 @@ function decodeEntity(source: string, index: number): {text: string; length: num if (!match) return undefined; if (match[3]) { const named = ENTITIES[match[3]]; return named ? {text: named, length: match[0].length} : undefined; } const code = match[1] ? Number(match[1]) : parseInt(match[2]!, 16); - // Controls and surrogates never come back through an entity. - if (!code || code < 0x20 || (code >= 0x7f && code < 0xa0) || (code >= 0xd800 && code <= 0xdfff) || code > 0x10ffff) return undefined; - return {text: String.fromCodePoint(code), length: match[0].length}; + if (!code || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) return undefined; + const text = String.fromCodePoint(code); + // An entity never smuggles in what the display scrubber removes: controls, bidi and other format characters + // (direction overrides, zero-width joiners), separators or private-use code points. + if (INVISIBLE.test(text)) return undefined; + return {text, length: match[0].length}; } /** Raw tokens before emphasis resolution: literal text, finished spans (code, links) and delimiter runs. */ @@ -57,10 +64,17 @@ type Token = /** The text of a bracket pair starting at `start` (`[`), honoring one level of nested brackets and escapes. */ function bracket(source: string, start: number): number { let depth = 0; - for (let index = start; index < source.length && index < start + 2000; index++) { + const limit = Math.min(source.length, start + 1000); + for (let index = start; index < limit; index++) { const char = source[index]; if (char === '\\') { index++; continue; } - if (char === '`') { const end = source.indexOf('`', index + 1); if (end > 0) index = end; continue; } + if (char === '`') { + // A code span inside link text may contain brackets; search for its end only inside this bounded window. + let end = index + 1; + while (end < limit && source[end] !== '`') end++; + if (end < limit) index = end; + continue; + } if (char === '[') depth++; else if (char === ']') { depth--; if (depth === 0) return index; } } @@ -134,8 +148,33 @@ function flanking(source: string, start: number, length: number, char: string): return {open: left, close: right}; } +/** Every backtick run's start, grouped by run length, for linear code-span matching. */ +function backtickRuns(source: string): Map { + const runs = new Map(); + for (let index = 0; index < source.length; index++) { + if (source[index] !== '`' || source[index - 1] === '`') continue; + let end = index; + while (source[end] === '`') end++; + const list = runs.get(end - index) ?? []; + list.push(index); + runs.set(end - index, list); + } + return runs; +} + function tokenize(source: string): Token[] { const tokens: Token[] = []; + const runs = backtickRuns(source); + /** Per run length, how many runs are already behind the scan position. */ + const consumed = new Map(); + const closingRun = (length: number, after: number): number => { + const list = runs.get(length); + if (!list) return -1; + let at = consumed.get(length) ?? 0; + while (at < list.length && list[at]! < after) at++; + consumed.set(length, at); + return at < list.length ? list[at]! : -1; + }; let text = ''; const flush = () => { if (text) { tokens.push({kind: 'text', text}); text = ''; } }; let index = 0; @@ -150,9 +189,8 @@ function tokenize(source: string): Token[] { let run = 1; while (source[index + run] === '`') run++; const fence = '`'.repeat(run); - let close = source.indexOf(fence, index + run); // The closing run must be exactly as long as the opening one. - while (close >= 0 && source[close + run] === '`') { let skip = close; while (source[skip] === '`') skip++; close = source.indexOf(fence, skip); } + const close = closingRun(run, index + run); if (close >= 0) { flush(); let code = source.slice(index + run, close).replace(/\n/gu, ' '); @@ -236,7 +274,8 @@ function resolve(tokens: Token[]): InlineSpan[] { let matched = node.close; while (matched && node.count > 0) { matched = false; - for (let depth = openers.length - 1; depth >= 0; depth--) { + // Emphasis pairs nearby delimiters; a bounded look-back keeps hostile delimiter floods linear. + for (let depth = openers.length - 1; depth >= Math.max(0, openers.length - MAX_OPENERS); depth--) { const opener = nodes[openers[depth]!] as Delim; if (opener.char !== node.char || opener.count === 0) continue; if (node.char === '~' && (opener.count < 2 || node.count < 2)) continue; @@ -255,15 +294,20 @@ function resolve(tokens: Token[]): InlineSpan[] { } const spans: InlineSpan[] = []; const active = {strong: 0, em: 0, strike: 0}; + const starting = new Map(), ending = new Map(); + for (const wrap of wraps) { + starting.set(wrap.from, [...(starting.get(wrap.from) ?? []), wrap]); + ending.set(wrap.to, [...(ending.get(wrap.to) ?? []), wrap]); + } for (let position = 0; position < nodes.length; position++) { // Leftover delimiter characters sit outside their pair: close before the closer, open after the opener. - for (const wrap of wraps) if (wrap.to === position) active[wrap.style]--; + for (const wrap of ending.get(position) ?? []) active[wrap.style]--; const node = nodes[position]!; const style: InlineStyle = {...(active.strong > 0 ? {strong: true} : {}), ...(active.em > 0 ? {em: true} : {}), ...(active.strike > 0 ? {strike: true} : {})}; if (node.kind === 'text') spans.push({text: node.text, style}); else if (node.kind === 'span') for (const span of node.span) spans.push({text: span.text, style: {...style, ...span.style}}); else if (node.count > 0) spans.push({text: node.char.repeat(node.count), style}); - for (const wrap of wraps) if (wrap.from === position) active[wrap.style]++; + for (const wrap of starting.get(position) ?? []) active[wrap.style]++; } // Merge neighbours with identical styling so wrapping sees whole words. const merged: InlineSpan[] = []; @@ -278,11 +322,18 @@ function resolve(tokens: Token[]): InlineSpan[] { /** Inline spans for one paragraph's source text. Soft line breaks become spaces; text past the bound stays plain. */ export function parseInline(source: string): InlineSpan[] { const bounded = source.length > MAX_INLINE ? source.slice(0, MAX_INLINE) : source; - const spans = resolve(tokenize(bounded.replace(/[ \t]*\n[ \t]*/gu, ' '))); - if (bounded.length < source.length) spans.push({text: source.slice(MAX_INLINE).replace(/\s*\n\s*/gu, ' '), style: {}}); + const spans = resolve(tokenize(softBreaks(bounded))); + if (bounded.length < source.length) spans.push({text: softBreaks(source.slice(MAX_INLINE)), style: {}}); return spans; } +/** Soft line breaks read as one space, with the spaces around them dropped (a linear scan, not a pattern). */ +function softBreaks(text: string): string { + if (!text.includes('\n')) return text; + const lines = text.split('\n'); + return lines.map((line, index) => index === 0 ? line.trimEnd() : index === lines.length - 1 ? line.trimStart() : line.trim()).join(' '); +} + /** The text a reader sees, without styling (search, copy-visible, width measurement). */ export function plainInline(source: string): string { return parseInline(source).map(span => span.text).join(''); diff --git a/src/markdown/render.ts b/src/markdown/render.ts index fe73f849..4f3bcdcb 100644 --- a/src/markdown/render.ts +++ b/src/markdown/render.ts @@ -4,6 +4,7 @@ import {getCurrentGlyphMode} from '../ui/glyphs.js'; import {UI_COLORS, type RgbColor} from '../ui/palette.js'; import type {SyntaxSgr} from '../input/syntaxTheme.js'; import {displayWidth, truncateAnsi} from '../util/text.js'; +import {stripTerminalControls} from '../util/terminalControls.js'; import {parseBlocks, type Alignment, type Block} from './blocks.js'; import {highlightCode, type CodeTokenKind} from './highlight.js'; import {parseInline, plainInline, type InlineSpan, type InlineStyle} from './inline.js'; @@ -75,7 +76,38 @@ function spanSgr(style: InlineStyle, context: Context, base: string): string { } function hostOf(url: string): string { - try { return new URL(url).host; } catch { return url; } + try { return new URL(url).host.toLowerCase(); } catch { return url; } +} + +/** Hostname-like words in link text, normalized the way URLs normalize them (lowercase, punycode for lookalikes). */ +function hostsIn(text: string): string[] { + const hosts: string[] = []; + for (const word of text.split(/[^A-Za-z0-9.\-\u00a0-\uffff]+/u)) { + let start = 0, end = word.length; + while (start < end && (word[start] === '.' || word[start] === '-')) start++; + while (end > start && (word[end - 1] === '.' || word[end - 1] === '-')) end--; + const candidate = word.slice(start, end); + if (!candidate.includes('.') || !/\p{L}/u.test(candidate) || candidate.length > 253) continue; + try { hosts.push(new URL(`http://${candidate}`).host.toLowerCase()); } catch { /* not a hostname */ } + } + return hosts; +} + +const bare = (host: string) => host.replace(/^www\./u, ''); + +/** + * Whether link text already shows exactly where the link goes. It must name the target's host as a whole hostname and + * name no other host: `[example.com](https://le.com)` or `[github.com login](https://github.com.evil.io)` reveal + * their real destination, while `[docs on example.com](https://example.com/docs)` needs nothing more. + */ +function textShowsHost(text: string, url: string): boolean { + const host = hostOf(url); + // An internationalized host can imitate another script's letters even when text and target agree: always reveal it. + if (host.split('.').some(label => label.startsWith('xn--'))) return false; + if (text === url) return true; + const target = bare(host); + const named = hostsIn(text).map(bare); + return named.length > 0 && named.every(host => host === target); } /** Spans as wrap atoms: words and spaces, each carrying its own SGR so every row can start fresh. */ @@ -95,12 +127,14 @@ function atomsOf(spans: readonly InlineSpan[], context: Context, base: string): const space = /^\s+$/u.test(part); atoms.push({text: space ? ' ' : part, sgr: space ? base : sgr, ...(link && !space ? {link} : {}), space}); } - // A link reveals where it goes: its host when the text does not already show it, or its URL without hyperlinks. + // A link reveals where it goes, judged on its whole text (it may span several styled pieces): the host when the + // text does not already show exactly that host, or the full URL when there are no hyperlinks to inspect. const next = spans[index + 1]; if (span.style.link && next?.style.link !== span.style.link) { - const host = hostOf(span.style.link); - const shown = span.text.includes(host) || span.text === span.style.link; - const reveal = shown ? '' : context.hyperlinks ? host : span.style.link; + let first = index; + while (first > 0 && spans[first - 1]!.style.link === span.style.link) first--; + const whole = spans.slice(first, index + 1).map(part => part.text).join(''); + const reveal = context.hyperlinks ? (textShowsHost(whole, span.style.link) ? '' : hostOf(span.style.link)) : whole === span.style.link ? '' : span.style.link; if (reveal) atoms.push({text: ' ', sgr: base, space: true}, {text: `(${reveal})`, sgr: subtle, space: false}); } } @@ -131,14 +165,16 @@ function wrapAtoms(atoms: readonly Atom[], width: number, first: string, rest: s const size = displayWidth(atom.text); if (!empty && used + size > width) push(); if (used + size > width) { - let chunk = ''; + // Hard-break a word wider than the row, measuring one character at a time (never re-measuring the chunk). + let chunk = '', chunkWidth = 0; for (const char of atom.text) { - if (used + displayWidth(chunk + char) > width && chunk) { - ansi += paintAtom({...atom, text: chunk}); plain += chunk; push(); chunk = ''; + const cell = displayWidth(char); + if (used + chunkWidth + cell > width && chunk) { + ansi += paintAtom({...atom, text: chunk}); plain += chunk; push(); chunk = ''; chunkWidth = 0; } - chunk += char; + chunk += char; chunkWidth += cell; } - ansi += paintAtom({...atom, text: chunk}); plain += chunk; used += displayWidth(chunk); empty = false; + ansi += paintAtom({...atom, text: chunk}); plain += chunk; used += chunkWidth; empty = false; continue; } ansi += paintAtom(atom); plain += atom.text; used += size; empty = false; @@ -343,7 +379,9 @@ export function renderMarkdown(source: string, columns: number, options: Markdow tone: options.tone === 'secondary' ? UI_COLORS.secondary : UI_COLORS.primary, codeIndex: 0, }; - const rows = renderBlocks(parseBlocks(source), width, context, 0); + // The renderer never trusts its caller to have scrubbed: terminal controls and bidi formatting go, tabs become spaces. + const safe = source.split('\n').map(line => stripTerminalControls(line.replace(/\t/gu, ' '), Number.MAX_SAFE_INTEGER)).join('\n'); + const rows = renderBlocks(parseBlocks(safe), width, context, 0); while (rows.length && rows.at(-1)!.kind === 'blank') rows.pop(); // A last line of defence: no row may exceed the width whatever a rule above computed. return rows.map(row => displayWidth(row.ansi) > width ? {...row, ansi: truncateAnsi(row.ansi, width)} : row); diff --git a/tests/markdownRender.test.ts b/tests/markdownRender.test.ts index 3368e291..36395c31 100644 --- a/tests/markdownRender.test.ts +++ b/tests/markdownRender.test.ts @@ -178,3 +178,45 @@ test('render: deep nesting and huge inputs stay bounded', () => { assert.equal(rows.length, 3000); assert.ok(Date.now() - started < 2000, 'thousands of rows render quickly'); }); + +test('security: link text never hides a different destination', () => { + const shown = (source: string) => renderMarkdown(source, 120, {level: 'truecolor', hyperlinks: true})[0]!.plain; + assert.equal(shown('[example.com](https://le.com)'), 'example.com (le.com)', 'a host that is only a substring of the text is revealed'); + assert.equal(shown('[github.com login](https://github.com.evil.io/login)'), 'github.com login (github.com.evil.io)'); + assert.equal(shown('[docs on example.com](https://example.com/docs)'), 'docs on example.com', 'text naming exactly the host needs nothing more'); + assert.equal(shown('[www.example.com](https://example.com)'), 'www.example.com'); + assert.equal(shown('[see example.com or other.org](https://example.com)'), 'see example.com or other.org (example.com)', 'naming another host reveals the real one'); + assert.equal(shown('[**git**hub.com](https://evil.com)'), 'github.com (evil.com)', 'the whole link text is judged, across styled pieces'); + assert.match(shown('[\u0440\u0430\u0443\u0440\u0430l.com](https://\u0440\u0430\u0443\u0440\u0430l.com)'), /\(xn--[a-z0-9-]+\.com\)$/u, 'an internationalized lookalike host is shown as punycode'); +}); + +test('security: entities and raw input never reintroduce controls, bidi or invisible characters', () => { + for (const entity of ['‮', '‮', '​', '', '⁦', '', '›', '', '
']) { + assert.equal(plainInline(`a${entity}b`), `a${entity}b`, `${entity} stays literal text`); + } + const rows = renderMarkdown('safe \u001b]0;pwned\u0007 text \u202Eevil\u202C and \u001b[31mred\u001b[0m', 80, {level: 'none'}); + assert.equal(rows[0]!.plain, 'safe text evil and red', 'the renderer scrubs its own input'); + assert.ok(!rows[0]!.ansi.includes('\u001b]0;') && !rows[0]!.ansi.includes('\u202E')); +}); + +test('security: pathological lines parse and render in linear time', () => { + const inputs = [ + `#${' '.repeat(60000)}x`, + `\`\`\`${' '.repeat(60000)}\`x`, + `${'- '.repeat(30000)}x`, + `a${' '.repeat(60000)}b`, + `a${' '.repeat(60000)}\nb`, + `\`${'\`\`'.repeat(20000)}`, + '['.repeat(40000), + '*a _'.repeat(15000), + `| ${'a |'.repeat(5000)}\n|${'---|'.repeat(5000)}`, + `[x](${'('.repeat(20000)}`, + `https://${'a.'.repeat(20000)}`, + `\`\`\`yaml\nkey${' '.repeat(3990)}\n\`\`\``, + ]; + for (const input of inputs) { + const started = Date.now(); + renderMarkdown(input, 80, {level: 'truecolor', hyperlinks: true}); + assert.ok(Date.now() - started < 1500, `${JSON.stringify(input.slice(0, 20))}… took ${Date.now() - started} ms`); + } +}); From 4a012ed7ae44c2b86523050e0a4324209e12c460 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:00:33 +0530 Subject: [PATCH 004/249] Managed Claude: a control plane and runtime telemetry with provenance The adapter now speaks the documented Agent SDK control requests over its existing stream-json channel, verified against Claude Code 2.1.295 by the control probe: - set_model, apply_flag_settings (session-only effortLevel), set_permission_mode, get_context_usage, mcp_status/toggle/reconnect and stop_task, each correlated by request id with a timeout, waiting for the initialize handshake. Bypassing every permission check is never sent; refusals are reported as the provider worded them. - The initialize response is kept: the model catalog with per-model effort levels, the provider's commands (built-in or not), agents, output styles and account plan. - Telemetry with provenance (provider, requested, observed, estimate): runtime facts from each turn's init, usage and cost, rate limits (shared only with targets of the same launch profile, labelled), compaction, retries, tasks, the TodoWrite plan and the files tools read and edited, with the provider's own patch hunks. - Structured tool results (patches, reads, command output, searches, plans, subagent reports), bounded, never keeping whole file contents. - Permission requests carry the provider's reason and its own rule offers; an accepted offer is returned verbatim, never composed by NMSh. - Streaming text (--include-partial-messages) is presentation only and is replaced by the completed message; a profile's effort is a launch flag. --- src/agents/sessions/claudeAdapter.ts | 196 +++++++++++++-- src/agents/sessions/claudeStream.ts | 343 +++++++++++++++++++++++++ src/agents/sessions/manager.ts | 94 ++++++- src/agents/sessions/model.ts | 59 ++++- src/agents/targets/adapter.ts | 2 +- src/agents/telemetry.ts | 361 +++++++++++++++++++++++++++ tests/agentControlPlane.test.ts | 252 +++++++++++++++++++ 7 files changed, 1272 insertions(+), 35 deletions(-) create mode 100644 src/agents/sessions/claudeStream.ts create mode 100644 src/agents/telemetry.ts create mode 100644 tests/agentControlPlane.test.ts diff --git a/src/agents/sessions/claudeAdapter.ts b/src/agents/sessions/claudeAdapter.ts index 36f2e4fb..515506d1 100644 --- a/src/agents/sessions/claudeAdapter.ts +++ b/src/agents/sessions/claudeAdapter.ts @@ -1,6 +1,8 @@ import {spawn, spawnSync, type ChildProcess} from 'node:child_process'; import {randomUUID} from 'node:crypto'; import type {AgentEvent, AgentQuestion} from './model.js'; +import {ClaudeStream, parseCatalog, parseContextUsage, parseSuggestions, parseToolResult, type PartialUpdate} from './claudeStream.js'; +import type {ContextSnapshot, TelemetryUpdate} from '../telemetry.js'; export function claudeQuestions(input: unknown): AgentQuestion[] | undefined { const value = input as {questions?: unknown} | undefined; @@ -32,6 +34,10 @@ export interface ClaudeCapabilities { hostPermissions: boolean; resume: boolean; sessionId: boolean; + /** `--include-partial-messages`: replies stream in as they are written (presentation only). */ + partialMessages?: boolean; + /** `--effort ` at launch. */ + effortFlag?: boolean; } const capabilityCache = new Map(); @@ -46,6 +52,8 @@ export function claudeCapabilities(executable: string, help?: string): ClaudeCap hostPermissions: /--permission-prompts[\s\S]{0,200}"host"/u.test(text), resume: /--resume\b/u.test(text), sessionId: /--session-id\b/u.test(text), + ...(/--include-partial-messages\b/u.test(text) ? {partialMessages: true} : {}), + ...(/--effort\b/u.test(text) ? {effortFlag: true} : {}), }; if (help === undefined) capabilityCache.set(executable, capabilities); return capabilities; @@ -92,11 +100,19 @@ export function resultFailure(message: Record): string | undefi return undefined; } +type ClaudeLine = {events: AgentEvent[]; control?: {requestId: string; subtype: string; tool?: string; input?: unknown}; turnError?: string}; + /** One stdout line → normalized events (unknown shapes produce nothing). */ -export function claudeEvents(line: string): {events: AgentEvent[]; control?: {requestId: string; subtype: string; tool?: string; input?: unknown}; turnError?: string} { - let message: Record; - try { message = JSON.parse(line) as Record; } catch { return {events: []}; } +export function claudeEvents(line: string): ClaudeLine { + let message: unknown; + try { message = JSON.parse(line); } catch { return {events: []}; } + return message && typeof message === 'object' && !Array.isArray(message) ? claudeMessageEvents(message as Record) : {events: []}; +} + +/** One parsed stream-json message → normalized events. */ +export function claudeMessageEvents(message: Record): ClaudeLine { const type = message.type; + const parent = typeof message.parent_tool_use_id === 'string' && message.parent_tool_use_id ? message.parent_tool_use_id.slice(0, 128) : undefined; if (type === 'system' && message.subtype === 'init') { const model = runtimeModel(message.model); return {events: [{kind: 'started', ...(typeof message.session_id === 'string' ? {harnessSessionId: message.session_id} : {})}, ...(model ? [{kind: 'model' as const, model}] : [])]}; @@ -112,7 +128,7 @@ export function claudeEvents(line: string): {events: AgentEvent[]; control?: {re if (block.type === 'text' && typeof block.text === 'string' && block.text.trim()) events.push({kind: 'assistant', text: block.text}); else if (block.type === 'tool_use' && typeof block.id === 'string' && typeof block.name === 'string') { const target = toolTarget(block.input); - events.push({kind: 'tool', id: block.id, name: block.name, ...(target ? {target} : {}), status: 'started', ...(block.input && typeof block.input === 'object' && !Array.isArray(block.input) ? {input: block.input as Record} : {})}); + events.push({kind: 'tool', id: block.id, name: block.name, ...(target ? {target} : {}), status: 'started', ...(block.input && typeof block.input === 'object' && !Array.isArray(block.input) ? {input: block.input as Record} : {}), ...(parent ? {parent} : {})}); } } return {events, ...(turnError ? {turnError} : {})}; @@ -120,11 +136,13 @@ export function claudeEvents(line: string): {events: AgentEvent[]; control?: {re if (type === 'user') { const content = (message.message as {content?: unknown} | undefined)?.content; const events: AgentEvent[] = []; + // One structured result per message: Claude Code sends each tool result as its own user message. + const result = parseToolResult(message.tool_use_result); for (const part of Array.isArray(content) ? content : []) { const block = part as {type?: unknown; tool_use_id?: unknown; is_error?: unknown; content?: unknown}; if (block.type === 'tool_result' && typeof block.tool_use_id === 'string') { const detail = textOf(block.content); - events.push({kind: 'tool', id: block.tool_use_id, name: '', status: block.is_error === true ? 'failed' : 'finished', ...(detail ? {detail} : {})}); + events.push({kind: 'tool', id: block.tool_use_id, name: '', status: block.is_error === true ? 'failed' : 'finished', ...(detail ? {detail} : {}), ...(result && block.is_error !== true ? {result} : {}), ...(parent ? {parent} : {})}); } } return {events}; @@ -135,12 +153,19 @@ export function claudeEvents(line: string): {events: AgentEvent[]; control?: {re return {events: [{kind: 'settled', ok, ...(reason ? {message: reason} : {})}]}; } if (type === 'control_request' && typeof message.request_id === 'string') { - const request = message.request as {subtype?: unknown; tool_name?: unknown; input?: unknown} | undefined; + const request = message.request as {subtype?: unknown; tool_name?: unknown; input?: unknown; tool_use_id?: unknown; title?: unknown; description?: unknown; + decision_reason?: unknown; permission_suggestions?: unknown; blocked_path?: unknown; default_to_no?: unknown} | undefined; const subtype = typeof request?.subtype === 'string' ? request.subtype : 'unknown'; const tool = typeof request?.tool_name === 'string' ? request.tool_name : undefined; const target = toolTarget(request?.input); const questions = tool === 'AskUserQuestion' ? claudeQuestions(request?.input) : undefined; - return {events: subtype === 'can_use_tool' && tool ? tool === 'AskUserQuestion' ? questions ? [{kind: 'choice', requestId: message.request_id, questions}] : [] : [{kind: 'approval', requestId: message.request_id, tool, ...(target ? {target} : {}), ...(request?.input && typeof request.input === 'object' ? {input: request.input as Record} : {})}] : [], + // What the provider says about the request (its own title and reason, and any rule it offers to remember). + const text = (value: unknown, limit: number) => typeof value === 'string' && value.trim() ? value.slice(0, limit) : undefined; + const suggestions = parseSuggestions(request?.permission_suggestions); + const extra = {...(text(request?.tool_use_id, 128) ? {toolUseId: text(request?.tool_use_id, 128)!} : {}), ...(text(request?.title ?? request?.description, 300) ? {title: text(request?.title ?? request?.description, 300)!} : {}), + ...(text(request?.decision_reason, 600) ? {reason: text(request?.decision_reason, 600)!} : {}), ...(suggestions.length ? {suggestions} : {}), + ...(text(request?.blocked_path, 4096) ? {blockedPath: text(request?.blocked_path, 4096)!} : {}), ...(request?.default_to_no === true ? {defaultNo: true} : {})}; + return {events: subtype === 'can_use_tool' && tool ? tool === 'AskUserQuestion' ? questions ? [{kind: 'choice', requestId: message.request_id, questions}] : [] : [{kind: 'approval', requestId: message.request_id, tool, ...(target ? {target} : {}), ...(request?.input && typeof request.input === 'object' ? {input: request.input as Record} : {}), ...extra}] : [], control: {requestId: message.request_id, subtype, ...(tool ? {tool} : {}), input: request?.input}}; } return {events: []}; @@ -154,9 +179,22 @@ export interface ClaudeSessionOptions { /** Extra provider-approved args from a launch profile (validated by the caller). */ args?: readonly string[]; env?: NodeJS.ProcessEnv; + /** A launch effort level (`--effort`), when the CLI documents it. */ + effort?: string; onEvent(event: AgentEvent): void; + /** Runtime facts with provenance: catalog, model, usage, context, limits, files, tasks. */ + onTelemetry?(update: TelemetryUpdate): void; + /** The reply streaming in now; presentation only, never source. Enables `--include-partial-messages`. */ + onPartial?(partial: PartialUpdate): void; } +/** The outcome of a control request: the provider's acknowledgement (and answer), or why there is none. */ +export type ControlResult = {ok: true; value: T} | {ok: false; reason: string}; + +/** Permission modes NMSh offers through set_permission_mode; bypassing all checks is never offered. */ +export const PERMISSION_MODES = ['default', 'acceptEdits', 'plan', 'auto', 'dontAsk'] as const; +export const EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'] as const; + /** One managed Claude Code process for one conversation. */ export class ClaudeSession { private child?: ChildProcess; @@ -175,12 +213,21 @@ export class ClaudeSession { private turnError?: string; private interruptRequested = false; private model?: string; + private readonly stream = new ClaudeStream(); + /** NMSh's own control requests awaiting the provider's response. */ + private readonly requests = new Map}>(); + private readonly ready: Promise; + private settleReady!: (ok: boolean) => void; readonly capabilities: ClaudeCapabilities; constructor(private readonly options: ClaudeSessionOptions, help?: string) { this.capabilities = claudeCapabilities(options.executable, help); + this.ready = new Promise(resolve => { this.settleReady = resolve; }); } + /** Whether the provider accepted the initialize handshake that control requests depend on. */ + get controllable(): boolean { return Boolean(this.child) && this.initialized && !this.initializationFailed && this.capabilities.hostPermissions; } + get pid(): number | undefined { return this.child?.pid; } start(): {ok: true} | {ok: false; reason: string} { @@ -189,6 +236,9 @@ export class ClaudeSession { // Host prompts let the person answer in NMSh; without them, anything that would prompt is denied by Claude itself. ...(this.capabilities.hostPermissions ? ['--permission-prompts', 'host', '--permission-prompt-tool', 'stdio'] : []), ...(this.options.resume && this.capabilities.resume ? ['--resume', this.options.resume] : this.capabilities.sessionId ? ['--session-id', randomUUID()] : []), + // Live text as it is written; the completed messages that follow remain the only source. + ...(this.options.onPartial && this.capabilities.partialMessages ? ['--include-partial-messages'] : []), + ...(this.options.effort && this.capabilities.effortFlag && (EFFORT_LEVELS as readonly string[]).includes(this.options.effort) ? ['--effort', this.options.effort] : []), ...(this.options.args ?? [])]; try { this.child = spawn(this.options.executable, args, {cwd: this.options.cwd, env: this.options.env ?? process.env, stdio: ['pipe', 'pipe', 'ignore']}); @@ -198,9 +248,10 @@ export class ClaudeSession { child.stdout!.on('data', (chunk: string) => this.read(chunk)); child.stdin!.on('drain', () => { this.writable = true; this.flush(); }); child.stdin!.on('error', () => { /* the exit event reports it */ }); - child.on('error', () => this.options.onEvent({kind: 'exited', code: -1})); - child.on('exit', code => { this.child = undefined; this.options.onEvent({kind: 'exited', code}); }); + child.on('error', () => { this.endRequests('The provider could not start'); this.options.onEvent({kind: 'exited', code: -1}); }); + child.on('exit', code => { this.child = undefined; this.endRequests('The provider exited'); this.settleReady(false); this.options.onEvent({kind: 'exited', code}); }); this.initialized = !this.capabilities.hostPermissions; + if (this.initialized) this.settleReady(false); if (!this.initialized) { this.initializeId = randomUUID(); this.write({type: 'control_request', request_id: this.initializeId, request: {subtype: 'initialize', hooks: {}}}); @@ -223,15 +274,35 @@ export class ClaudeSession { this.buffer = this.buffer.slice(newline + 1); if (Buffer.byteLength(line) > 8 * 1024 * 1024) {this.options.onEvent({kind: 'incomplete', reason: 'Provider frame exceeds 8 MiB safety limit'}); continue;} if (!line) continue; - let raw: {type?: string; response?: {request_id?: string; subtype?: string}}; - try {raw = JSON.parse(line);} catch {continue;} + let parsed: unknown; + try {parsed = JSON.parse(line);} catch {continue;} + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) continue; + const message = parsed as Record; + const raw = message as {type?: string; response?: {request_id?: string; subtype?: string; response?: unknown}}; if (raw.type === 'control_response' && raw.response?.request_id === this.initializeId) { clearTimeout(this.initializeTimer); - if (raw.response?.subtype === 'success') {this.initialized = true; for (const text of this.messages.splice(0)) if (!this.sendFrame(text)) this.options.onEvent({kind: 'incomplete', reason: 'Queued message was not submitted'});} - else this.failInitialization('Provider initialization was rejected'); + if (raw.response?.subtype === 'success') { + this.initialized = true; + this.settleReady(true); + // The provider's model catalog, commands and account plan, as it published them. + const catalog = parseCatalog(raw.response.response, Date.now()); + if (catalog) this.options.onTelemetry?.({kind: 'catalog', catalog}); + for (const text of this.messages.splice(0)) if (!this.sendFrame(text)) this.options.onEvent({kind: 'incomplete', reason: 'Queued message was not submitted'}); + } else this.failInitialization('Provider initialization was rejected'); + continue; + } + const parsedStream = this.stream.parse(message); + if (parsedStream.response) { + const waiting = this.requests.get(parsedStream.response.requestId); + if (waiting) { + this.requests.delete(parsedStream.response.requestId); clearTimeout(waiting.timer); + waiting.resolve(parsedStream.response.ok ? {ok: true, value: parsedStream.response.body} : {ok: false, reason: parsedStream.response.error ?? 'The provider refused the request'}); + } continue; } - const {events, control, turnError} = claudeEvents(line); + for (const update of parsedStream.telemetry) this.options.onTelemetry?.(update); + if (parsedStream.partial) this.options.onPartial?.(parsedStream.partial); + const {events, control, turnError} = claudeMessageEvents(message); if (turnError) this.turnError = turnError; for (let event of events) { if (event.kind === 'model') { if (event.model === this.model) continue; this.model = event.model; } @@ -255,9 +326,95 @@ export class ClaudeSession { private failInitialization(reason: string): void { this.initializationFailed = true; this.messages = []; + this.settleReady(false); this.options.onEvent({kind: 'settled', ok: false, message: reason}); this.close(); } + private endRequests(reason: string): void { + for (const [id, waiting] of this.requests) { clearTimeout(waiting.timer); waiting.resolve({ok: false, reason}); this.requests.delete(id); } + } + + /** + * One control request (the documented Agent SDK wire format), answered by the provider's control_response or a + * timeout. Requests wait for the initialize handshake; nothing is sent to a provider that never accepted it. + */ + async request(body: Record, timeoutMs = 15000): Promise { + if (!this.child || this.initializationFailed) return {ok: false, reason: 'The provider is not running'}; + if (!this.capabilities.hostPermissions) return {ok: false, reason: 'This Claude Code version does not document the control channel NMSh uses'}; + if (!this.initialized && !(await Promise.race([this.ready, new Promise(resolve => setTimeout(() => resolve(false), timeoutMs).unref())]))) { + return {ok: false, reason: 'The provider has not finished starting'}; + } + if (this.requests.size >= 32) return {ok: false, reason: 'Too many requests are waiting for the provider'}; + const id = randomUUID(); + return new Promise(resolve => { + const timer = setTimeout(() => { if (this.requests.delete(id)) resolve({ok: false, reason: 'The provider did not answer in time'}); }, timeoutMs); + timer.unref(); + this.requests.set(id, {resolve, timer}); + if (!this.write({type: 'control_request', request_id: id, request: body})) { this.requests.delete(id); clearTimeout(timer); resolve({ok: false, reason: 'The provider input queue is full'}); } + }); + } + + /** Change the model for later turns (set_model); undefined restores the provider's default. */ + async setModel(model: string | undefined): Promise { + if (model !== undefined && !/^[\w.:@/\-[\]]{1,96}$/u.test(model)) return {ok: false, reason: 'Not a model identifier'}; + const result = await this.request({subtype: 'set_model', model: model ?? 'default'}); + if (result.ok) this.acknowledged('model', model ?? 'default'); + return result; + } + + /** Session-only effort (apply_flag_settings effortLevel); null returns to the model's default. Never written to settings files. */ + async setEffort(level: string | null): Promise { + if (level !== null && !(EFFORT_LEVELS as readonly string[]).includes(level)) return {ok: false, reason: 'Not an effort level'}; + const result = await this.request({subtype: 'apply_flag_settings', settings: {effortLevel: level}}); + if (result.ok) this.acknowledged('effort', level); + return result; + } + + /** The session's permission mode (set_permission_mode). Bypassing all checks is never sent. */ + async setPermissionMode(mode: string): Promise { + if (!(PERMISSION_MODES as readonly string[]).includes(mode)) return {ok: false, reason: 'Not a permission mode NMSh offers'}; + const result = await this.request({subtype: 'set_permission_mode', mode}); + if (result.ok) this.acknowledged('permissionMode', mode); + return result; + } + + /** The provider's own breakdown of the context window. `full` asks the provider to count each category (slower, may call its API). */ + async contextUsage(detail: 'summary' | 'full' = 'summary'): Promise> { + const result = await this.request({subtype: 'get_context_usage', detail}, detail === 'full' ? 45000 : 15000); + if (!result.ok) return result; + const snapshot = parseContextUsage(result.value, detail); + if (!snapshot) return {ok: false, reason: 'The provider answered in a shape NMSh does not know'}; + this.options.onTelemetry?.({kind: 'context', snapshot, at: Date.now()}); + return {ok: true, value: snapshot}; + } + + /** MCP servers as the provider sees them now (mcp_status). */ + async mcpStatus(): Promise> { + const result = await this.request({subtype: 'mcp_status'}); + if (!result.ok) return result; + const servers = result.value && typeof result.value === 'object' && Array.isArray((result.value as {mcpServers?: unknown}).mcpServers) ? (result.value as {mcpServers: unknown[]}).mcpServers : undefined; + return servers ? {ok: true, value: servers.slice(0, 64)} : {ok: false, reason: 'The provider answered in a shape NMSh does not know'}; + } + + async mcpToggle(name: string, enabled: boolean): Promise { + return this.request({subtype: 'mcp_toggle', serverName: name.slice(0, 128), enabled}); + } + + async mcpReconnect(name: string): Promise { + return this.request({subtype: 'mcp_reconnect', serverName: name.slice(0, 128)}, 30000); + } + + async stopTask(taskId: string): Promise { + return this.request({subtype: 'stop_task', task_id: taskId.slice(0, 128)}); + } + + private acknowledged(field: 'model' | 'effort' | 'permissionMode', value: string | null): void { + const at = Date.now(); + this.options.onTelemetry?.({kind: 'requested', field, value, at}); + const label = field === 'model' ? `Model set to ${value}` : field === 'effort' ? value ? `Effort set to ${value}` : 'Effort back to the model default' : `Permission mode set to ${value}`; + this.options.onEvent({kind: 'control', field, value, label}); + } + private write(message: unknown, control = true): boolean { const frame = `${JSON.stringify(message)}\n`; // Reserve sixteen slots and one MiB for human control responses under backpressure. @@ -287,14 +444,17 @@ export class ClaudeSession { return this.write({type: 'user', message: {role: 'user', content: [{type: 'text', text}]}}, false); } - /** Answer one pending permission prompt with the person's explicit choice; never automatic. */ - answer(requestId: string, allow: boolean): boolean { + /** + * Answer one pending permission prompt with the person's explicit choice; never automatic. `remember` is one of the + * rule changes the provider itself offered with this request, returned verbatim (NMSh never composes a rule). + */ + answer(requestId: string, allow: boolean, remember?: {update: Record; label: string}): boolean { const pending = this.pending.get(requestId); if (!pending || pending.questions || !this.child) return false; if (!this.write({type: 'control_response', response: {subtype: 'success', request_id: requestId, - response: allow ? {behavior: 'allow', updatedInput: pending.input ?? {}} : {behavior: 'deny', message: 'Denied in NMSh.'}}})) return false; + response: allow ? {behavior: 'allow', updatedInput: pending.input ?? {}, ...(remember ? {updatedPermissions: [remember.update]} : {})} : {behavior: 'deny', message: 'Denied in NMSh.'}}})) return false; this.pending.delete(requestId); - this.options.onEvent({kind: 'approvalAnswered', requestId, allowed: allow}); + this.options.onEvent({kind: 'approvalAnswered', requestId, allowed: allow, ...(allow && remember ? {remembered: remember.label} : {})}); return true; } diff --git a/src/agents/sessions/claudeStream.ts b/src/agents/sessions/claudeStream.ts new file mode 100644 index 00000000..de9654ff --- /dev/null +++ b/src/agents/sessions/claudeStream.ts @@ -0,0 +1,343 @@ +import type {AgentEvent, PermissionSuggestion, ToolResult} from './model.js'; +import type {Catalog, ContextSnapshot, PatchHunk, RuntimeInfo, TelemetryUpdate, TodoItem} from '../telemetry.js'; + +/** + * Claude Code's stream-json messages (the documented Agent SDK wire format) normalized into NMSh's provider-neutral + * shapes: telemetry updates with provenance, structured tool results and presentation-only streaming text. Every + * field is validated individually and bounded; an unknown or malformed field is dropped, never guessed. Strings stay + * raw here and are scrubbed for display where they are painted. + */ + +const MAX_TEXT = 64 * 1024; +const MAX_PATCH_LINES = 2000; +const MAX_LINE = 2000; + +export const isRecord = (value: unknown): value is Record => typeof value === 'object' && value !== null && !Array.isArray(value); +const str = (value: unknown, limit = 256): string | undefined => typeof value === 'string' && value.length > 0 ? value.slice(0, limit) : undefined; +const num = (value: unknown, max = Number.MAX_SAFE_INTEGER): number | undefined => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= max ? value : undefined; +const strings = (value: unknown, count = 256, limit = 128): string[] => Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string').slice(0, count).map(item => item.slice(0, limit)) : []; + +/** Keep the beginning and the end of long output (where errors and summaries live), marking the cut. */ +export function boundText(text: string, limit = MAX_TEXT): {text: string; truncated: boolean} { + if (text.length <= limit) return {text, truncated: false}; + const head = Math.floor(limit / 4); + return {text: `${text.slice(0, head)}\n… ${text.length - limit} characters not kept …\n${text.slice(text.length - (limit - head))}`, truncated: true}; +} + +// ---- Initialization ------------------------------------------------------------------------------------------------ + +export function parseCatalog(response: unknown, at: number): Catalog | undefined { + if (!isRecord(response)) return undefined; + const models = Array.isArray(response.models) ? response.models.flatMap(item => { + if (!isRecord(item) || !str(item.value, 96) || !str(item.displayName, 96)) return []; + const levels = strings(item.supportedEffortLevels, 8, 16); + return [{value: str(item.value, 96)!, name: str(item.displayName, 96)!, ...(str(item.resolvedModel, 96) ? {resolved: str(item.resolvedModel, 96)!} : {}), + ...(str(item.description, 300) ? {description: str(item.description, 300)!} : {}), ...(item.supportsEffort === true && levels.length ? {effortLevels: levels} : {}), + ...(item.supportsFastMode === true ? {fastMode: true} : {})}]; + }).slice(0, 64) : []; + const commands = Array.isArray(response.commands) ? response.commands.flatMap(item => { + if (!isRecord(item) || !str(item.name, 64) || !/^[\w:.\-]+$/u.test(item.name as string)) return []; + return [{name: str(item.name, 64)!, description: str(item.description, 300) ?? '', ...(str(item.argumentHint, 160) ? {argumentHint: str(item.argumentHint, 160)!} : {}), + ...(strings(item.aliases, 8, 64).length ? {aliases: strings(item.aliases, 8, 64)} : {}), builtin: item.builtin === true}]; + }).slice(0, 400) : []; + const account = isRecord(response.account) ? response.account : undefined; + return { + models, commands, + agents: Array.isArray(response.agents) ? response.agents.flatMap(item => isRecord(item) && str(item.name, 64) ? [str(item.name, 64)!] : []).slice(0, 64) : [], + outputStyles: strings(response.available_output_styles, 32, 64), + ...(str(response.output_style, 64) ? {outputStyle: str(response.output_style, 64)!} : {}), + ...(account ? {account: {...(str(account.apiProvider, 32) ? {provider: str(account.apiProvider, 32)!} : {}), ...(str(account.subscriptionType, 64) ? {plan: str(account.subscriptionType, 64)!} : {}), + ...(str(account.email, 254) ? {email: str(account.email, 254)!} : {}), ...(str(account.organization, 128) ? {organization: str(account.organization, 128)!} : {})}} : {}), + at, + }; +} + +function parseRuntime(message: Record, at: number): RuntimeInfo { + const effort = message.effort === null ? null : str(message.effort, 16); + return { + ...(str(message.model, 96) ? {model: str(message.model, 96)!} : {}), + ...(str(message.permissionMode, 32) ? {permissionMode: str(message.permissionMode, 32)!} : {}), + tools: strings(message.tools, 256, 128), + mcpServers: Array.isArray(message.mcp_servers) ? message.mcp_servers.flatMap(item => isRecord(item) && str(item.name, 128) ? [{name: str(item.name, 128)!, status: str(item.status, 32) ?? 'unknown', ...(str(item.source, 32) ? {source: str(item.source, 32)!} : {})}] : []).slice(0, 64) : [], + slashCommands: strings(message.slash_commands, 400, 64), + skills: strings(message.skills, 400, 64), + plugins: Array.isArray(message.plugins) ? message.plugins.flatMap(item => isRecord(item) && str(item.name, 128) ? [{name: str(item.name, 128)!, ...(str(item.version, 32) ? {version: str(item.version, 32)!} : {})}] : []).slice(0, 128) : [], + ...(str(message.output_style, 64) ? {outputStyle: str(message.output_style, 64)!} : {}), + ...(str(message.claude_code_version, 32) ? {version: str(message.claude_code_version, 32)!} : {}), + ...(effort !== undefined ? {effort} : {}), + capabilities: strings(message.capabilities, 64, 64), + at, + }; +} + +export function parseContextUsage(response: unknown, detail: 'summary' | 'full'): ContextSnapshot | undefined { + if (!isRecord(response) || num(response.totalTokens) === undefined || num(response.maxTokens) === undefined) return undefined; + const kinds = new Set(['used', 'free', 'buffer', 'deferred']); + const breakdown = isRecord(response.messageBreakdown) ? response.messageBreakdown : undefined; + const apiUsage = isRecord(response.apiUsage) ? response.apiUsage : undefined; + const skills = isRecord(response.skills) ? response.skills : undefined; + return { + ...(str(response.model, 96) ? {model: str(response.model, 96)!} : {}), + totalTokens: num(response.totalTokens)!, maxTokens: num(response.maxTokens)!, percentage: num(response.percentage, 100) ?? 0, + categories: Array.isArray(response.categories) ? response.categories.flatMap(item => isRecord(item) && str(item.name, 64) && num(item.tokens) !== undefined && kinds.has(item.kind as string) + ? [{name: str(item.name, 64)!, tokens: num(item.tokens)!, kind: item.kind as ContextSnapshot['categories'][number]['kind']}] : []).slice(0, 32) : [], + ...(num(response.autoCompactThreshold) !== undefined ? {autoCompactThreshold: num(response.autoCompactThreshold)!} : {}), + autoCompact: response.isAutoCompactEnabled === true, + memoryFiles: Array.isArray(response.memoryFiles) ? response.memoryFiles.flatMap(item => isRecord(item) && str(item.path, 1024) ? [{path: str(item.path, 1024)!, type: str(item.type, 32) ?? '', tokens: num(item.tokens) ?? 0}] : []).slice(0, 64) : [], + mcpTools: Array.isArray(response.mcpTools) ? response.mcpTools.flatMap(item => isRecord(item) && str(item.name, 128) ? [{name: str(item.name, 128)!, server: str(item.serverName, 128) ?? '', tokens: num(item.tokens) ?? 0}] : []).slice(0, 256) : [], + agents: Array.isArray(response.agents) ? response.agents.flatMap(item => isRecord(item) && str(item.agentType, 64) ? [{type: str(item.agentType, 64)!, source: str(item.source, 32) ?? '', tokens: num(item.tokens) ?? 0}] : []).slice(0, 64) : [], + ...(skills && num(skills.totalSkills) !== undefined ? {skills: {total: num(skills.totalSkills)!, included: num(skills.includedSkills) ?? 0, tokens: num(skills.tokens) ?? 0}} : {}), + ...(breakdown ? {messages: {toolCalls: num(breakdown.toolCallTokens) ?? 0, toolResults: num(breakdown.toolResultTokens) ?? 0, attachments: num(breakdown.attachmentTokens) ?? 0, + assistant: num(breakdown.assistantMessageTokens) ?? 0, user: num(breakdown.userMessageTokens) ?? 0, + byTool: Array.isArray(breakdown.toolCallsByType) ? breakdown.toolCallsByType.flatMap(item => isRecord(item) && str(item.name, 128) ? [{name: str(item.name, 128)!, call: num(item.callTokens) ?? 0, result: num(item.resultTokens) ?? 0}] : []).slice(0, 64) : []}} : {}), + ...(apiUsage ? {apiUsage: {input: num(apiUsage.input_tokens) ?? 0, output: num(apiUsage.output_tokens) ?? 0, cacheRead: num(apiUsage.cache_read_input_tokens) ?? 0, cacheWrite: num(apiUsage.cache_creation_input_tokens) ?? 0}} : {}), + detail, + }; +} + +// ---- Tool results -------------------------------------------------------------------------------------------------- + +function parsePatch(value: unknown): {hunks: PatchHunk[]; added: number; removed: number; truncated: boolean} | undefined { + if (!Array.isArray(value)) return undefined; + const hunks: PatchHunk[] = []; + let added = 0, removed = 0, lines = 0, truncated = false; + for (const item of value.slice(0, 200)) { + if (!isRecord(item) || !Array.isArray(item.lines)) continue; + const kept: string[] = []; + for (const line of item.lines) { + if (typeof line !== 'string') continue; + if (line.startsWith('+')) added++; else if (line.startsWith('-')) removed++; + if (lines >= MAX_PATCH_LINES) { truncated = true; continue; } + kept.push(line.length > MAX_LINE ? `${line.slice(0, MAX_LINE)}…` : line); + lines++; + } + hunks.push({oldStart: num(item.oldStart) ?? 0, oldLines: num(item.oldLines) ?? 0, newStart: num(item.newStart) ?? 0, newLines: num(item.newLines) ?? 0, lines: kept}); + } + if (value.length > 200) truncated = true; + return {hunks, added, removed, truncated}; +} + +/** A tool's structured output by its shape (the documented *Output types). Contents of whole files are never kept. */ +export function parseToolResult(value: unknown): ToolResult | undefined { + if (!isRecord(value)) return undefined; + if (typeof value.filePath === 'string' && Array.isArray(value.structuredPatch)) { + const patch = parsePatch(value.structuredPatch)!; + const created = value.type === 'create'; + // A created file's patch may be empty; its size is the written content's line count. + const added = created && !patch.added && typeof value.content === 'string' ? value.content.split('\n').length : patch.added; + return {kind: 'patch', path: value.filePath.slice(0, 4096), hunks: patch.hunks, added, removed: patch.removed, created, truncated: patch.truncated}; + } + if (isRecord(value.file) && typeof value.file.filePath === 'string') { + const file = value.file; + return {kind: 'read', path: String(file.filePath).slice(0, 4096), ...(num(file.numLines) !== undefined ? {lines: num(file.numLines)!} : {}), + ...(num(file.startLine) !== undefined ? {start: num(file.startLine)!} : {}), ...(num(file.totalLines) !== undefined ? {total: num(file.totalLines)!} : {}), + ...(value.type === 'image' ? {image: true} : {})}; + } + if (typeof value.stdout === 'string' || typeof value.stderr === 'string') { + const out = boundText(typeof value.stdout === 'string' ? value.stdout : ''); + const err = boundText(typeof value.stderr === 'string' ? value.stderr : '', 16 * 1024); + return {kind: 'bash', stdout: out.text, stderr: err.text, interrupted: value.interrupted === true, truncated: out.truncated || err.truncated, + ...(str(value.backgroundTaskId, 64) ? {background: str(value.backgroundTaskId, 64)!} : {})}; + } + if (num(value.numFiles) !== undefined && Array.isArray(value.filenames)) { + return {kind: 'search', files: num(value.numFiles)!, ...(num(value.numMatches) !== undefined ? {matches: num(value.numMatches)!} : {}), + ...(value.truncated === true ? {truncated: true} : {}), names: strings(value.filenames, 100, 1024)}; + } + if (Array.isArray(value.newTodos)) { + const todos = parseTodos(value.newTodos); + return todos ? {kind: 'todos', todos} : undefined; + } + if (typeof value.agentId === 'string' && Array.isArray(value.content)) { + const text = value.content.map(part => isRecord(part) && part.type === 'text' && typeof part.text === 'string' ? part.text : '').join('\n'); + return {kind: 'agent', text: boundText(text).text, ...(num(value.totalTokens) !== undefined ? {tokens: num(value.totalTokens)!} : {}), + ...(num(value.totalToolUseCount) !== undefined ? {toolUses: num(value.totalToolUseCount)!} : {}), ...(num(value.totalDurationMs) !== undefined ? {durationMs: num(value.totalDurationMs)!} : {}), + ...(str(value.agentType, 64) ? {agentType: str(value.agentType, 64)!} : {})}; + } + return undefined; +} + +export function parseTodos(value: unknown): TodoItem[] | undefined { + if (!Array.isArray(value)) return undefined; + const statuses = new Set(['pending', 'in_progress', 'completed']); + const todos = value.flatMap(item => isRecord(item) && typeof item.content === 'string' && statuses.has(item.status as string) + ? [{content: item.content.slice(0, 500), status: item.status as TodoItem['status'], ...(str(item.activeForm, 500) ? {activeForm: str(item.activeForm, 500)!} : {})}] : []).slice(0, 100); + return todos; +} + +/** Permission rule offers in plain words. Only typed fields are read; anything unrecognized is not offered. */ +export function parseSuggestions(value: unknown): PermissionSuggestion[] { + if (!Array.isArray(value)) return []; + return value.slice(0, 8).flatMap(item => { + if (!isRecord(item) || typeof item.type !== 'string' || typeof item.destination !== 'string') return []; + const where = item.destination === 'session' ? 'for this session' : item.destination === 'localSettings' ? 'in this project (local settings)' + : item.destination === 'projectSettings' ? 'in this project (shared settings)' : item.destination === 'userSettings' ? 'for every project (user settings)' : undefined; + if (!where) return []; + let label: string | undefined; + if (item.type === 'addRules' && Array.isArray(item.rules) && item.behavior === 'allow') { + const rules = item.rules.flatMap(rule => isRecord(rule) && typeof rule.toolName === 'string' ? [`${rule.toolName}${typeof rule.ruleContent === 'string' && rule.ruleContent ? `(${rule.ruleContent})` : ''}`] : []); + if (rules.length) label = `Allow ${rules.join(', ').slice(0, 200)} ${where}`; + } else if (item.type === 'setMode' && typeof item.mode === 'string') label = `Switch to ${item.mode} mode ${where}`; + else if (item.type === 'addDirectories' && Array.isArray(item.directories)) label = `Allow access to ${strings(item.directories, 4, 512).join(', ')} ${where}`; + return label ? [{update: item, label, destination: item.destination}] : []; + }); +} + +// ---- Stream parser ------------------------------------------------------------------------------------------------- + +export interface PartialUpdate { + /** The text of the content block streaming now, or undefined to clear it. */ + text?: string; + thinking?: boolean; +} + +export interface ClaudeParse { + events: AgentEvent[]; + telemetry: TelemetryUpdate[]; + partial?: PartialUpdate; + /** A response to one of NMSh's own control requests. */ + response?: {requestId: string; ok: boolean; body?: unknown; error?: string}; +} + +/** Tool names a tool result needs (file activity, todos) are remembered per tool_use id, bounded. */ +export class ClaudeStream { + private readonly tools = new Map}>(); + private partial = ''; + private streamingParent: string | null = null; + + constructor(private readonly now: () => number = Date.now) {} + + parse(message: Record): ClaudeParse { + const at = this.now(); + const out: ClaudeParse = {events: [], telemetry: []}; + const type = message.type; + if (type === 'control_response' && isRecord(message.response) && typeof message.response.request_id === 'string') { + const response = message.response; + out.response = {requestId: response.request_id as string, ok: response.subtype === 'success', + ...(response.subtype === 'success' ? {body: response.response} : {error: str(response.error, 300) ?? 'The provider refused the request'})}; + return out; + } + if (type === 'system') { + switch (message.subtype) { + case 'init': out.telemetry.push({kind: 'runtime', runtime: parseRuntime(message, at)}); break; + case 'status': + out.telemetry.push({kind: 'status', status: str(message.status, 32) ?? null, ...(str(message.permissionMode, 32) ? {permissionMode: str(message.permissionMode, 32)!} : {}), at}); + break; + case 'compact_boundary': { + const meta = isRecord(message.compact_metadata) ? message.compact_metadata : {}; + const pre = num(meta.pre_tokens); + if (pre !== undefined) { + const post = num(meta.post_tokens); + const trigger = str(meta.trigger, 16) ?? 'auto'; + out.telemetry.push({kind: 'compaction', compaction: {at, trigger, preTokens: pre, ...(post !== undefined ? {postTokens: post} : {})}}); + out.events.push({kind: 'compacted', trigger, preTokens: pre, ...(post !== undefined ? {postTokens: post} : {})}); + } + break; + } + case 'api_retry': { + const attempt = num(message.attempt, 1000), max = num(message.max_retries, 1000); + if (attempt !== undefined && max !== undefined) out.telemetry.push({kind: 'retry', attempt, max, delayMs: num(message.retry_delay_ms) ?? 0, ...(num(message.error_status, 999) ? {status: num(message.error_status, 999)!} : {}), at}); + break; + } + case 'task_started': case 'task_progress': case 'task_updated': case 'task_notification': { + const id = str(message.task_id, 128); + if (!id || message.ambient === true || message.skip_transcript === true) break; + const usage = isRecord(message.usage) ? message.usage : undefined; + const patch = isRecord(message.patch) ? message.patch : undefined; + const status = message.subtype === 'task_started' ? 'running' : message.subtype === 'task_notification' ? str(message.status, 16) : str(patch?.status, 16); + out.telemetry.push({kind: 'task', at, task: {id, ...(str(message.description ?? patch?.description, 300) ? {description: str(message.description ?? patch?.description, 300)!} : {}), + ...(str(message.task_type, 32) ? {type: str(message.task_type, 32)!} : {}), ...(str(message.subagent_type, 64) ? {subagent: str(message.subagent_type, 64)!} : {}), + ...(status ? {status} : {}), ...(str(message.tool_use_id, 128) ? {toolUseId: str(message.tool_use_id, 128)!} : {}), + ...(str(message.parent_task_id, 128) ? {parent: str(message.parent_task_id, 128)!} : {}), ...(str(message.summary, 500) ? {summary: str(message.summary, 500)!} : {}), + ...(str(message.last_tool_name, 64) ? {lastTool: str(message.last_tool_name, 64)!} : {}), + ...(usage ? {...(num(usage.total_tokens) !== undefined ? {tokens: num(usage.total_tokens)!} : {}), ...(num(usage.tool_uses) !== undefined ? {toolUses: num(usage.tool_uses)!} : {}), + ...(num(usage.duration_ms) !== undefined ? {durationMs: num(usage.duration_ms)!} : {})} : {}), + ...(typeof message.is_backgrounded === 'boolean' ? {background: message.is_backgrounded} : patch && typeof patch.is_backgrounded === 'boolean' ? {background: patch.is_backgrounded} : {})}}); + break; + } + } + return out; + } + if (type === 'rate_limit_event' && isRecord(message.rate_limit_info)) { + const info = message.rate_limit_info; + const kind = str(info.rateLimitType, 48) ?? 'unknown'; + const status = str(info.status, 32); + if (status) out.telemetry.push({kind: 'rateLimit', type: kind, at, limit: {status, ...(num(info.utilization, 10) !== undefined ? {utilization: num(info.utilization, 10)!} : {}), + ...(num(info.resetsAt) !== undefined ? {resetsAt: num(info.resetsAt)! * (num(info.resetsAt)! < 1e12 ? 1000 : 1)} : {})}}); + return out; + } + if (type === 'result') { + const usage = isRecord(message.usage) ? message.usage : {}; + const perModel: Record = {}; + if (isRecord(message.modelUsage)) for (const [model, value] of Object.entries(message.modelUsage).slice(0, 16)) { + if (!isRecord(value) || !/^[\w.:@/\-[\]]{1,96}$/u.test(model)) continue; + perModel[model] = {input: num(value.inputTokens) ?? 0, output: num(value.outputTokens) ?? 0, cacheRead: num(value.cacheReadInputTokens) ?? 0, cacheWrite: num(value.cacheCreationInputTokens) ?? 0, + ...(num(value.costUSD) !== undefined ? {costUsd: num(value.costUSD)!} : {}), ...(num(value.contextWindow) ? {contextWindow: num(value.contextWindow)!} : {}), + ...(num(value.maxOutputTokens) ? {maxOutput: num(value.maxOutputTokens)!} : {})}; + } + out.telemetry.push({kind: 'usage', at, usage: {perModel, ...(num(message.total_cost_usd) !== undefined ? {costUsd: num(message.total_cost_usd)!} : {}), + lastTurn: {input: num(usage.input_tokens) ?? 0, output: num(usage.output_tokens) ?? 0, cacheRead: num(usage.cache_read_input_tokens) ?? 0, cacheWrite: num(usage.cache_creation_input_tokens) ?? 0}, + turns: num(message.num_turns) ?? 0, durationMs: num(message.duration_ms) ?? 0, apiMs: num(message.duration_api_ms) ?? 0}}); + out.telemetry.push({kind: 'settled', at}); + this.partial = ''; + out.partial = {}; + return out; + } + if (type === 'stream_event' && isRecord(message.event)) { + // Streaming is presentation: the completed assistant message that follows is the only source. + const parent = typeof message.parent_tool_use_id === 'string' ? message.parent_tool_use_id : null; + const event = message.event; + if (event.type === 'message_start') { this.partial = ''; this.streamingParent = parent; return out; } + if (parent !== this.streamingParent || parent) return out; + if (event.type === 'content_block_start' && isRecord(event.content_block)) { + if (event.content_block.type === 'thinking' || event.content_block.type === 'redacted_thinking') out.partial = {thinking: true}; + else if (event.content_block.type === 'text') { this.partial = ''; out.partial = {text: ''}; } + } else if (event.type === 'content_block_delta' && isRecord(event.delta) && event.delta.type === 'text_delta' && typeof event.delta.text === 'string') { + if (this.partial.length < 512 * 1024) this.partial += event.delta.text; + out.partial = {text: this.partial}; + } + return out; + } + if (type === 'assistant' || type === 'user') { + const parent = typeof message.parent_tool_use_id === 'string' ? message.parent_tool_use_id.slice(0, 128) : undefined; + const content = isRecord(message.message) && Array.isArray(message.message.content) ? message.message.content : []; + if (type === 'assistant') { + for (const part of content) { + if (!isRecord(part) || part.type !== 'tool_use' || typeof part.id !== 'string' || typeof part.name !== 'string') continue; + const input = isRecord(part.input) ? part.input : undefined; + if (this.tools.size > 4096) this.tools.delete(this.tools.keys().next().value!); + this.tools.set(part.id, {name: part.name, ...(input ? {input} : {})}); + out.telemetry.push(...this.toolStarted(part.name, input, at)); + } + if (content.some(part => isRecord(part) && part.type === 'text') && !parent) { this.partial = ''; out.partial = {}; } + } else { + const result = parseToolResult(message.tool_use_result); + for (const part of content) { + if (!isRecord(part) || part.type !== 'tool_result' || typeof part.tool_use_id !== 'string') continue; + const tool = this.tools.get(part.tool_use_id); + if (result?.kind === 'patch' && part.is_error !== true) { + out.telemetry.push({kind: 'file', path: result.path, action: tool?.name === 'Write' || result.created ? 'write' : 'edit', added: result.added, removed: result.removed, patch: result.hunks, at}); + } + } + } + return out; + } + return out; + } + + /** The structured result for a tool_result block in a user message, when the message carries one. */ + resultFor(message: Record): ToolResult | undefined { + return parseToolResult(message.tool_use_result); + } + + private toolStarted(name: string, input: Record | undefined, at: number): TelemetryUpdate[] { + const updates: TelemetryUpdate[] = []; + const path = typeof input?.file_path === 'string' ? input.file_path : typeof input?.notebook_path === 'string' ? input.notebook_path : undefined; + // Reads are counted when requested; edits and writes when their result reports the patch (a denied edit changes nothing). + if (path && name === 'Read') updates.push({kind: 'file', path: path.slice(0, 4096), action: 'read', at}); + if (name === 'TodoWrite') { + const todos = parseTodos(input?.todos); + if (todos) updates.push({kind: 'todos', todos, at}); + } + return updates; + } +} diff --git a/src/agents/sessions/manager.ts b/src/agents/sessions/manager.ts index 3838de6b..6e7ae41c 100644 --- a/src/agents/sessions/manager.ts +++ b/src/agents/sessions/manager.ts @@ -2,7 +2,8 @@ import {assignSignature} from '../../session/signatures.js'; import {basename, isAbsolute} from 'node:path'; import {resolveCommand} from '../../providers/providers.js'; import {harness, HARNESSES, type HarnessDescriptor} from '../harnesses.js'; -import {ClaudeSession} from './claudeAdapter.js'; +import {ClaudeSession, EFFORT_LEVELS, type ControlResult} from './claudeAdapter.js'; +import {applyTelemetry, emptyTelemetry, type ContextSnapshot, type TelemetryUpdate} from '../telemetry.js'; import type {TargetAdapter} from '../targets/adapter.js'; import {capabilityFacts, updateCapabilities} from '../targets/capabilities.js'; import {AgentTranscript} from '../transcript/model.js'; @@ -22,6 +23,8 @@ export interface AgentProfile { model?: string; /** Claude: --permission-mode, one of the modes the CLI documents (never bypassPermissions). */ permissionMode?: 'acceptEdits' | 'auto' | 'manual' | 'dontAsk' | 'plan'; + /** Claude: --effort at launch (session-only in Claude; NMSh never writes Claude's settings). */ + effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max'; /** Claude: CLAUDE_CONFIG_DIR, the harness's own config/account directory selector. */ configDir?: string; } @@ -41,6 +44,7 @@ export function normalizeProfiles(value: unknown): AgentProfile[] { if (typeof item.model === 'string' && /^[\w.:@-]{1,64}$/u.test(item.model)) profile.model = item.model; if (typeof item.permissionMode === 'string' && PERMISSION_MODES.has(item.permissionMode)) profile.permissionMode = item.permissionMode as AgentProfile['permissionMode']; if (typeof item.configDir === 'string' && isAbsolute(item.configDir) && !/[\n\u0000]/u.test(item.configDir)) profile.configDir = item.configDir; + if (typeof item.effort === 'string' && (EFFORT_LEVELS as readonly string[]).includes(item.effort)) profile.effort = item.effort as AgentProfile['effort']; } profiles.push(profile); } @@ -101,6 +105,7 @@ export class AgentSessions { const profile = options.profile?.harness === descriptor.id ? options.profile : undefined; if (profile) session.profileId = profile.name; session.capabilities = capabilityFacts(id, 'managed', descriptor.id); + session.telemetry = emptyTelemetry(); try {session.transcript = new AgentTranscript();} catch {return {ok: false, reason: 'Cannot create private agent source storage.'};} if (profile) this.profiles.set(id, profile); const started = this.startControl(session, profile); @@ -115,8 +120,18 @@ export class AgentSessions { const executable = this.env.resolve('claude'); if (!executable || !session.cwd) return {ok: false, reason: 'Claude executable or workspace unavailable'}; const args = [...(profile?.model ? ['--model', profile.model] : []), ...(profile?.permissionMode ? ['--permission-mode', profile.permissionMode] : [])]; + session.telemetry ??= emptyTelemetry(); const control = new ClaudeSession({executable, cwd: session.cwd, args, ...(resume ? {resume} : {}), ...(profile?.configDir ? {env: {...process.env, CLAUDE_CONFIG_DIR: profile.configDir}} : {}), - onEvent: event => { if (event.kind === 'incomplete') session.transcript?.source.markIncomplete(event.reason); pushEvent(session, event, this.env.now()); updateCapabilities(session.capabilities!, event, this.env.now()); if (event.kind === 'started') session.reconnectable = Boolean(event.harnessSessionId && control.capabilities.resume); if (event.kind === 'exited') this.controls.delete(session.id); this.emit(session, event); }}, this.env.claudeHelp); + ...(profile?.effort ? {effort: profile.effort} : {}), + onEvent: event => { if (event.kind === 'incomplete') session.transcript?.source.markIncomplete(event.reason); pushEvent(session, event, this.env.now()); updateCapabilities(session.capabilities!, event, this.env.now()); if (event.kind === 'started') session.reconnectable = Boolean(event.harnessSessionId && control.capabilities.resume); if (event.kind === 'exited') this.controls.delete(session.id); this.emit(session, event); }, + onTelemetry: update => this.telemetry(session, update), + onPartial: partial => { + // Presentation only: the streaming text is replaced by the provider's completed message, never stored. + const now = this.env.now(); + session.partial = partial.thinking ? {text: '', at: now, thinking: true} : partial.text === undefined ? undefined : {text: partial.text, at: now}; + this.emitPartial(session); + }}, this.env.claudeHelp); + if (profile?.effort) applyTelemetry(session.telemetry, {kind: 'requested', field: 'effort', value: profile.effort, at: this.env.now()}); const started = control.start(); if (!started.ok) return started; session.pid = control.pid; @@ -133,6 +148,69 @@ export class AgentSessions { return this.startControl(session, this.profiles.get(id), session.harnessSessionId); } + /** Apply one telemetry update; account-wide rate limits also reach live targets of the same launch profile, labelled. */ + private telemetry(session: AgentSession, update: TelemetryUpdate): void { + session.telemetry ??= emptyTelemetry(); + applyTelemetry(session.telemetry, update); + if (update.kind === 'rateLimit') { + const key = session.profileId ?? ''; + for (const other of this.sessions) { + if (other === session || other.harness !== session.harness || (other.profileId ?? '') !== key || !other.telemetry) continue; + applyTelemetry(other.telemetry, {...update, limit: {...update.limit, from: session.title}}); + } + } + this.emit(session); + } + + private partialListeners = new Set<(session: AgentSession) => void>(); + /** Streaming text changes often; hosts subscribe separately so they can coalesce frames. */ + onPartial(listener: (session: AgentSession) => void): () => void { + this.partialListeners.add(listener); + return () => this.partialListeners.delete(listener); + } + private emitPartial(session: AgentSession): void { for (const listener of this.partialListeners) listener(session); } + + /** The live control channel of a managed target, when its provider accepted one. */ + private control(id: string): ClaudeSession | undefined { + const control = this.controls.get(id); + return control instanceof ClaudeSession ? control : undefined; + } + + /** Whether a target can take runtime controls (model, effort, permission mode, context, MCP) right now. */ + controllable(id: string): boolean { return Boolean(this.control(id)?.controllable); } + + async setModel(id: string, model: string | undefined): Promise { + return this.control(id)?.setModel(model) ?? {ok: false, reason: 'This target has no live control channel'}; + } + + async setEffort(id: string, level: string | null): Promise { + return this.control(id)?.setEffort(level) ?? {ok: false, reason: 'This target has no live control channel'}; + } + + async setPermissionMode(id: string, mode: string): Promise { + return this.control(id)?.setPermissionMode(mode) ?? {ok: false, reason: 'This target has no live control channel'}; + } + + async contextUsage(id: string, detail: 'summary' | 'full' = 'summary'): Promise> { + return this.control(id)?.contextUsage(detail) ?? {ok: false, reason: 'This target has no live control channel'}; + } + + async mcpStatus(id: string): Promise> { + return this.control(id)?.mcpStatus() ?? {ok: false, reason: 'This target has no live control channel'}; + } + + async mcpToggle(id: string, name: string, enabled: boolean): Promise { + return this.control(id)?.mcpToggle(name, enabled) ?? {ok: false, reason: 'This target has no live control channel'}; + } + + async mcpReconnect(id: string, name: string): Promise { + return this.control(id)?.mcpReconnect(name) ?? {ok: false, reason: 'This target has no live control channel'}; + } + + async stopTask(id: string, taskId: string): Promise { + return this.control(id)?.stopTask(taskId) ?? {ok: false, reason: 'This target has no live control channel'}; + } + /** Text for a managed session; observed sessions never accept input. */ send(id: string, text: string): boolean { const session = this.get(id); @@ -142,12 +220,17 @@ export class AgentSessions { return control.send(text); } - /** The person's explicit answer to a pending approval; NMSh never answers on its own. */ - answer(id: string, allow: boolean): boolean { + /** + * The person's explicit answer to a pending approval; NMSh never answers on its own. `remember` selects one of the + * rule changes the provider offered with this very request (by index), never one NMSh composed. + */ + answer(id: string, allow: boolean, remember?: number): boolean { const session = this.get(id); const pending = session?.pendingApproval; const control = this.controls.get(id); - return Boolean(session && pending && control && control.answer(pending.requestId, allow)); + const suggestion = remember === undefined ? undefined : pending?.suggestions?.[remember]; + if (remember !== undefined && !suggestion) return false; + return Boolean(session && pending && control && control.answer(pending.requestId, allow, suggestion)); } choose(id: string, requestId: string, answers: Record): boolean { @@ -208,5 +291,6 @@ export class AgentSessions { for (const session of this.sessions) session.transcript?.dispose(); this.profiles.clear(); this.listeners.clear(); + this.partialListeners.clear(); } } diff --git a/src/agents/sessions/model.ts b/src/agents/sessions/model.ts index f71d0eeb..4489fa12 100644 --- a/src/agents/sessions/model.ts +++ b/src/agents/sessions/model.ts @@ -13,6 +13,28 @@ export type AgentState = 'starting' | 'working' | 'waiting' | 'approval' | 'choi export interface AgentQuestion {question: string; header: string; options: Array<{label: string; description: string}>; multiSelect: boolean} +/** + * A tool's structured result as the provider reported it (Claude Code's `tool_use_result`), bounded and normalized. + * Only facts the provider supplied: a diff is the provider's own patch hunks, never one NMSh computed. + */ +export type ToolResult = + | {kind: 'patch'; path: string; hunks: import('../telemetry.js').PatchHunk[]; added: number; removed: number; created: boolean; truncated: boolean} + | {kind: 'read'; path: string; lines?: number; start?: number; total?: number; image?: boolean} + | {kind: 'bash'; stdout: string; stderr: string; interrupted: boolean; background?: string; truncated: boolean} + | {kind: 'search'; files: number; matches?: number; truncated?: boolean; names: string[]} + | {kind: 'todos'; todos: import('../telemetry.js').TodoItem[]} + | {kind: 'agent'; text: string; tokens?: number; toolUses?: number; durationMs?: number; agentType?: string}; + +/** A rule change the provider offers alongside a permission request ("allow this command for the session"). */ +export interface PermissionSuggestion { + /** The provider's own update object, returned verbatim when the person chooses it. */ + update: Record; + /** Plain words for the offer, built from the update's typed fields. */ + label: string; + /** Session-only offers change nothing on disk; others write the named settings file. */ + destination: string; +} + export type AgentEvent = | {kind: 'requestsCleared'} | {kind: 'choice'; requestId: string; questions: AgentQuestion[]} @@ -21,10 +43,15 @@ export type AgentEvent = | {kind: 'started'; harnessSessionId?: string} | {kind: 'user'; text: string} | {kind: 'assistant'; text: string} - /** A tool call; `target` is a factual file/command from the harness event, never inferred. */ - | {kind: 'tool'; id: string; name: string; target?: string; status: 'started' | 'finished' | 'failed'; detail?: string; input?: Record} - | {kind: 'approval'; requestId: string; tool: string; target?: string; input?: Record} - | {kind: 'approvalAnswered'; requestId: string; allowed: boolean} + /** A tool call; `target` is a factual file/command from the harness event, never inferred. `parent` is the subagent's spawning tool call. */ + | {kind: 'tool'; id: string; name: string; target?: string; status: 'started' | 'finished' | 'failed'; detail?: string; input?: Record; result?: ToolResult; parent?: string} + | {kind: 'approval'; requestId: string; tool: string; target?: string; input?: Record; toolUseId?: string; title?: string; reason?: string; + suggestions?: PermissionSuggestion[]; blockedPath?: string; defaultNo?: boolean} + | {kind: 'approvalAnswered'; requestId: string; allowed: boolean; /** The rule change the person accepted with it, in words. */ remembered?: string} + /** NMSh changed a runtime setting through a supported control and the provider acknowledged it. */ + | {kind: 'control'; field: 'model' | 'effort' | 'permissionMode'; value: string | null; label: string} + /** The provider compacted the conversation (its context was summarized). */ + | {kind: 'compacted'; trigger: string; preTokens: number; postTokens?: number} /** A finished turn. `message` is a factual failure reason from structured provider fields only; `interrupted` when the person interrupted it. */ | {kind: 'settled'; ok: boolean; message?: string; interrupted?: boolean} /** The model the provider reports it is running (structured runtime evidence, never inferred). */ @@ -57,7 +84,14 @@ export interface AgentSession { reconnectable?: boolean; /** The runtime model from the provider's own structured events; absent until reported. */ model?: string; - pendingApproval?: {requestId: string; tool: string; target?: string; input?: Record}; + pendingApproval?: {requestId: string; tool: string; target?: string; input?: Record; toolUseId?: string; title?: string; reason?: string; + suggestions?: PermissionSuggestion[]; blockedPath?: string; defaultNo?: boolean}; + /** Runtime facts with provenance (model, effort, context, usage, limits, files, tasks); absent on legacy fixtures. */ + telemetry?: import('../telemetry.js').AgentTelemetry; + /** The reply streaming in now (presentation only, never source): replaced by the provider's completed message. */ + partial?: {text: string; at: number; thinking?: boolean}; + /** When the current turn started, for elapsed time in working state. */ + turnStartedAt?: number; /** Meaningful news the user has not acknowledged (approval, waiting, finished, failed, exited). */ attention: boolean; updatedAt: number; @@ -94,7 +128,7 @@ export function pushEvent(session: AgentSession, event: AgentEvent, now = Date.n session.transcript?.append(event); // This array is only the recent presentation cache; complete source lives in transcript. const excerpt = (text: string) => text.length > 4000 ? `${text.slice(0, 4000)}\n[excerpt · use eligible source store]` : text; - const preview = event.kind === 'tool' ? {kind: event.kind, id: event.id, name: event.name, target: event.target, status: event.status, ...(event.detail ? {detail: excerpt(event.detail)} : {})} + const preview = event.kind === 'tool' ? {kind: event.kind, id: event.id, name: event.name, target: event.target, status: event.status, ...(event.detail ? {detail: excerpt(event.detail)} : {}), ...(event.parent ? {parent: event.parent} : {})} : event.kind === 'approval' ? {kind: event.kind, requestId: event.requestId, tool: event.tool, target: event.target} : event.kind === 'user' || event.kind === 'assistant' ? {...event, text: excerpt(event.text)} : event.kind === 'choice' ? {...event, questions: event.questions.map(q => ({...q, question: q.question.slice(0, 200), options: q.options.map(o => ({label: o.label.slice(0, 100), description: o.description.slice(0, 200)}))}))} @@ -111,16 +145,19 @@ export function pushEvent(session: AgentSession, event: AgentEvent, now = Date.n session.updatedAt = now; switch (event.kind) { case 'started': if (event.harnessSessionId) session.harnessSessionId = event.harnessSessionId; break; - case 'user': session.state = 'working'; session.activity = undefined; session.attention = false; break; - case 'assistant': if (session.state !== 'approval') session.state = 'working'; break; + case 'user': session.state = 'working'; session.activity = undefined; session.attention = false; session.turnStartedAt = now; break; case 'tool': session.activity = event.status === 'started' ? toolActivity(event.name, event.target) : undefined; if (session.state !== 'approval') session.state = 'working'; break; - case 'approval': session.state = 'approval'; session.pendingApproval = {requestId: event.requestId, tool: event.tool, ...(event.target ? {target: event.target} : {}), ...(event.input ? {input: event.input} : {})}; session.attention = true; break; + case 'approval': { + const {kind: _kind, ...request} = event; + session.state = 'approval'; session.pendingApproval = request; session.attention = true; break; + } case 'choice': session.pendingChoice = {requestId: event.requestId, questions: event.questions}; session.state = 'choice'; session.attention = true; break; case 'choiceAnswered': session.pendingChoice = undefined; session.state = 'working'; break; case 'approvalAnswered': session.pendingApproval = undefined; session.state = 'working'; break; case 'requestsCleared': session.pendingApproval = undefined; session.pendingChoice = undefined; break; - case 'settled': session.state = 'waiting'; session.pendingApproval = undefined; session.pendingChoice = undefined; session.activity = undefined; session.attention = true; break; - case 'exited': session.state = event.code === 0 ? 'exited' : 'failed'; session.activity = undefined; session.attention = true; session.pendingApproval = undefined; session.pendingChoice = undefined; break; + case 'settled': session.state = 'waiting'; session.pendingApproval = undefined; session.pendingChoice = undefined; session.activity = undefined; session.attention = true; session.partial = undefined; session.turnStartedAt = undefined; break; + case 'exited': session.state = event.code === 0 ? 'exited' : 'failed'; session.activity = undefined; session.attention = true; session.pendingApproval = undefined; session.pendingChoice = undefined; session.partial = undefined; session.turnStartedAt = undefined; break; + case 'assistant': if (session.state !== 'approval') session.state = 'working'; session.partial = undefined; break; case 'renamed': session.title = event.title; break; case 'model': session.model = event.model; break; } diff --git a/src/agents/targets/adapter.ts b/src/agents/targets/adapter.ts index 7941856e..5815a89e 100644 --- a/src/agents/targets/adapter.ts +++ b/src/agents/targets/adapter.ts @@ -3,7 +3,7 @@ export interface TargetAdapter { readonly pid: number | undefined; start(): {ok: true} | {ok: false; reason: string}; send(text: string): boolean; - answer(requestId: string, allow: boolean): boolean; + answer(requestId: string, allow: boolean, remember?: {update: Record; label: string}): boolean; choose?(requestId: string, answers: Record): boolean; cancel(): void; close(): void; diff --git a/src/agents/telemetry.ts b/src/agents/telemetry.ts new file mode 100644 index 00000000..d90eb752 --- /dev/null +++ b/src/agents/telemetry.ts @@ -0,0 +1,361 @@ +import {displayText} from './transcript/projection.js'; + +/** + * What NMSh knows about one managed target's runtime: its model and effort, permission mode, context window, usage, + * rate limits, compaction, the files its tools touched, its tasks and its plan. Every value says where it came from + * and when, so the interface can tell a provider's own report from something NMSh observed, requested or estimated. + * Telemetry is target-scoped (account-scoped rate limits are copied between targets of the same launch profile only, + * labelled as such) and display-only: it is never written to shell history, journals or exported settings. + */ + +/** Where a fact came from. */ +export type Provenance = + /** The provider reported it in a structured event or control response. */ + | 'provider' + /** NMSh asked for it and the provider acknowledged the request; the effect is not separately reported. */ + | 'requested' + /** NMSh saw it happen (a tool event naming a file, a turn's timing). */ + | 'observed' + /** NMSh derived it from other reported numbers; shown as an estimate. */ + | 'estimate'; + +export interface Sourced { + value: T; + source: Provenance; + at: number; + /** Short words for detail views: "system/init", "set_model acknowledged", "rate_limit_event". */ + evidence: string; +} + +export interface ModelOption { + /** What the provider accepts (an alias such as "sonnet" or a full id). */ + value: string; + /** The model the alias resolves to now, when the provider says. */ + resolved?: string; + name: string; + description?: string; + effortLevels?: readonly string[]; + fastMode?: boolean; +} + +export interface ProviderCommand { + name: string; + description: string; + argumentHint?: string; + aliases?: readonly string[]; + /** The provider's own built-in, as opposed to a user, project, plugin or MCP-defined command. */ + builtin: boolean; +} + +export interface Catalog { + models: ModelOption[]; + commands: ProviderCommand[]; + agents: string[]; + outputStyles: string[]; + outputStyle?: string; + /** Account facts the provider published at initialization; the e-mail is kept in memory only and shown on request. */ + account?: {provider?: string; plan?: string; email?: string; organization?: string}; + at: number; +} + +export interface RuntimeInfo { + model?: string; + permissionMode?: string; + tools: string[]; + mcpServers: Array<{name: string; status: string; source?: string}>; + slashCommands: string[]; + skills: string[]; + plugins: Array<{name: string; version?: string}>; + outputStyle?: string; + version?: string; + /** Only when the provider reports it; headless Claude Code does not. */ + effort?: string | null; + capabilities: string[]; + at: number; +} + +export interface ModelUsageTotals { + input: number; + output: number; + cacheRead: number; + cacheWrite: number; + costUsd?: number; + contextWindow?: number; + maxOutput?: number; +} + +export interface UsageTotals { + /** Cumulative across the target's turns (the provider reports running totals). */ + perModel: Record; + costUsd?: number; + /** The newest turn's main-loop request: what the context held at that call. */ + lastTurn?: {input: number; output: number; cacheRead: number; cacheWrite: number; model?: string}; + turns: number; + durationMs: number; + apiMs: number; + at: number; +} + +export interface ContextCategory { + name: string; + tokens: number; + kind: 'used' | 'free' | 'buffer' | 'deferred'; +} + +export interface ContextSnapshot { + model?: string; + totalTokens: number; + maxTokens: number; + percentage: number; + categories: ContextCategory[]; + autoCompactThreshold?: number; + autoCompact: boolean; + memoryFiles: Array<{path: string; type: string; tokens: number}>; + mcpTools: Array<{name: string; server: string; tokens: number}>; + agents: Array<{type: string; source: string; tokens: number}>; + skills?: {total: number; included: number; tokens: number}; + messages?: {toolCalls: number; toolResults: number; attachments: number; assistant: number; user: number; byTool: Array<{name: string; call: number; result: number}>}; + apiUsage?: {input: number; output: number; cacheRead: number; cacheWrite: number}; + detail: 'summary' | 'full'; +} + +export interface RateLimit { + status: string; + utilization?: number; + resetsAt?: number; + /** Which target reported it, when copied from another target of the same launch profile. */ + from?: string; +} + +export interface Compaction { + at: number; + trigger: string; + preTokens: number; + postTokens?: number; +} + +export interface FileActivity { + path: string; + reads: number; + edits: number; + writes: number; + added: number; + removed: number; + last: number; + /** The newest edit's provider-reported patch (bounded), for the right panel and diff detail. */ + patch?: PatchHunk[]; +} + +export interface PatchHunk { + oldStart: number; + oldLines: number; + newStart: number; + newLines: number; + lines: string[]; +} + +export interface TaskInfo { + id: string; + description: string; + type?: string; + subagent?: string; + status: string; + toolUseId?: string; + parent?: string; + summary?: string; + lastTool?: string; + tokens?: number; + toolUses?: number; + durationMs?: number; + background?: boolean; + started: number; + updated: number; +} + +export interface TodoItem { + content: string; + status: 'pending' | 'in_progress' | 'completed'; + activeForm?: string; +} + +export interface AgentTelemetry { + catalog?: Catalog; + runtime?: RuntimeInfo; + /** Requests NMSh made through supported controls, with the provider's acknowledgement. */ + requested: {model?: Sourced; effort?: Sourced; permissionMode?: Sourced}; + modelHistory: Array<{at: number; model: string; source: Provenance}>; + usage?: UsageTotals; + context?: Sourced; + rateLimits: Record>; + compactions: Compaction[]; + files: Map; + tasks: Map; + todos?: Sourced; + /** The provider is retrying an API call (attempt, of, delay); cleared when a turn settles. */ + retry?: {attempt: number; max: number; delayMs: number; status?: number; at: number}; + /** "compacting" while the provider compacts, from system/status. */ + status?: Sourced; +} + +export function emptyTelemetry(): AgentTelemetry { + return {requested: {}, modelHistory: [], rateLimits: {}, compactions: [], files: new Map(), tasks: new Map()}; +} + +/** Normalized updates an adapter emits; the reducer below is the only writer of telemetry. */ +export type TelemetryUpdate = + | {kind: 'catalog'; catalog: Catalog} + | {kind: 'runtime'; runtime: RuntimeInfo} + | {kind: 'usage'; usage: Omit; at: number} + | {kind: 'context'; snapshot: ContextSnapshot; at: number} + | {kind: 'rateLimit'; type: string; limit: RateLimit; at: number} + | {kind: 'compaction'; compaction: Compaction} + | {kind: 'status'; status: string | null; permissionMode?: string; at: number} + | {kind: 'retry'; attempt: number; max: number; delayMs: number; status?: number; at: number} + | {kind: 'task'; task: Partial & {id: string}; at: number} + | {kind: 'todos'; todos: TodoItem[]; at: number} + | {kind: 'file'; path: string; action: 'read' | 'edit' | 'write'; added?: number; removed?: number; patch?: PatchHunk[]; at: number} + | {kind: 'requested'; field: 'model' | 'effort' | 'permissionMode'; value: string | null; at: number} + | {kind: 'settled'; at: number}; + +const MAX_FILES = 500; +const MAX_TASKS = 200; +const MAX_HISTORY = 50; + +export function applyTelemetry(telemetry: AgentTelemetry, update: TelemetryUpdate): void { + switch (update.kind) { + case 'catalog': telemetry.catalog = update.catalog; break; + case 'runtime': { + const previous = telemetry.runtime?.model; + telemetry.runtime = update.runtime; + if (update.runtime.model && update.runtime.model !== previous) pushHistory(telemetry, update.runtime.model, 'provider', update.runtime.at); + if (update.runtime.permissionMode && telemetry.requested.permissionMode?.value === update.runtime.permissionMode) { + // The provider now reports the mode NMSh asked for: the report is authoritative from here on. + telemetry.requested.permissionMode = undefined; + } + break; + } + case 'usage': telemetry.usage = {...update.usage, at: update.at}; break; + case 'context': telemetry.context = {value: update.snapshot, source: 'provider', at: update.at, evidence: `get_context_usage (${update.snapshot.detail})`}; break; + case 'rateLimit': telemetry.rateLimits[update.type] = {value: update.limit, source: 'provider', at: update.at, evidence: update.limit.from ? `rate_limit_event from ${update.limit.from}` : 'rate_limit_event'}; break; + case 'compaction': + telemetry.compactions.push(update.compaction); + if (telemetry.compactions.length > MAX_HISTORY) telemetry.compactions.shift(); + // The window was rebuilt: an older breakdown no longer describes it. + telemetry.context = undefined; + break; + case 'status': + telemetry.status = update.status ? {value: update.status, source: 'provider', at: update.at, evidence: 'system/status'} : undefined; + if (update.permissionMode && telemetry.runtime) telemetry.runtime = {...telemetry.runtime, permissionMode: update.permissionMode, at: update.at}; + break; + case 'retry': telemetry.retry = {attempt: update.attempt, max: update.max, delayMs: update.delayMs, ...(update.status ? {status: update.status} : {}), at: update.at}; break; + case 'task': { + const existing = telemetry.tasks.get(update.task.id); + const next: TaskInfo = {description: '', status: 'running', started: update.at, ...existing, ...update.task, updated: update.at}; + telemetry.tasks.set(update.task.id, next); + if (telemetry.tasks.size > MAX_TASKS) telemetry.tasks.delete(telemetry.tasks.keys().next().value!); + break; + } + case 'todos': telemetry.todos = {value: update.todos, source: 'provider', at: update.at, evidence: 'TodoWrite'}; break; + case 'file': { + const key = update.path; + const file = telemetry.files.get(key) ?? {path: key, reads: 0, edits: 0, writes: 0, added: 0, removed: 0, last: update.at}; + if (update.action === 'read') file.reads++; + else if (update.action === 'edit') file.edits++; + else file.writes++; + file.added += update.added ?? 0; + file.removed += update.removed ?? 0; + file.last = update.at; + if (update.patch) file.patch = update.patch; + telemetry.files.delete(key); + telemetry.files.set(key, file); + if (telemetry.files.size > MAX_FILES) telemetry.files.delete(telemetry.files.keys().next().value!); + break; + } + case 'requested': { + const evidence = update.field === 'model' ? 'set_model acknowledged' : update.field === 'effort' ? 'apply_flag_settings acknowledged' : 'set_permission_mode acknowledged'; + if (update.field === 'model' && update.value) pushHistory(telemetry, update.value, 'requested', update.at); + (telemetry.requested as Record>)[update.field] = {value: update.value, source: 'requested', at: update.at, evidence}; + break; + } + case 'settled': telemetry.retry = undefined; break; + } +} + +function pushHistory(telemetry: AgentTelemetry, model: string, source: Provenance, at: number): void { + const last = telemetry.modelHistory.at(-1); + if (last?.model === model) return; + telemetry.modelHistory.push({at, model, source}); + if (telemetry.modelHistory.length > MAX_HISTORY) telemetry.modelHistory.shift(); +} + +// ---- Derived views ------------------------------------------------------------------------------------------------ + +export interface ContextMeter { + used: number; + max: number; + percent: number; + source: Provenance; + at: number; + evidence: string; +} + +/** + * How full the context window is now. The provider's own breakdown wins; otherwise the newest turn's main-loop request + * (input plus cache reads and writes) is the context the model last saw, an estimate against the reported window. + * Lifetime totals are never used here: they measure spend, not what the model holds. + */ +export function contextMeter(telemetry: AgentTelemetry): ContextMeter | undefined { + const usage = telemetry.usage; + const context = telemetry.context; + const turnAfterSnapshot = usage && context && usage.at > context.at; + if (context && !turnAfterSnapshot && context.value.maxTokens > 0) { + return {used: context.value.totalTokens, max: context.value.maxTokens, percent: Math.min(100, (context.value.totalTokens / context.value.maxTokens) * 100), + source: 'provider', at: context.at, evidence: context.evidence}; + } + const last = usage?.lastTurn; + const model = last?.model ?? telemetry.runtime?.model; + const window = (model ? usage?.perModel[model]?.contextWindow : undefined) ?? Object.values(usage?.perModel ?? {}).find(item => item.contextWindow)?.contextWindow ?? context?.value.maxTokens; + if (!last || !window) return undefined; + const used = last.input + last.cacheRead + last.cacheWrite + last.output; + return {used, max: window, percent: Math.min(100, (used / window) * 100), source: 'estimate', at: usage!.at, evidence: 'newest turn usage against the reported window'}; +} + +/** The model the provider runs now, else the one NMSh requested (pending the next report), else none. */ +export function currentModel(telemetry: AgentTelemetry, reported?: string): Sourced | undefined { + const requested = telemetry.requested.model; + const runtime = telemetry.runtime; + if (requested && (!runtime?.model || requested.at > runtime.at) && !sameModel(requested.value, runtime?.model, telemetry)) return requested; + const model = runtime?.model ?? reported; + return model ? {value: model, source: 'provider', at: runtime?.at ?? 0, evidence: 'system/init'} : undefined; +} + +/** Whether an alias such as "sonnet" names the runtime model id the provider reports. */ +export function sameModel(requested: string | undefined, runtime: string | undefined, telemetry: AgentTelemetry): boolean { + if (!requested || !runtime) return false; + if (requested === runtime) return true; + const option = telemetry.catalog?.models.find(model => model.value === requested); + return option?.resolved === runtime; +} + +/** A display name for a model id: the catalog's name ("Sonnet 5.5"), else a tidy form of the id. */ +export function modelName(model: string, telemetry: AgentTelemetry | undefined): string { + const option = telemetry?.catalog?.models.find(item => item.resolved === model && item.value !== 'default') ?? telemetry?.catalog?.models.find(item => item.value === model); + if (option) return displayText(option.name).replace(/\s*\(recommended\)$/iu, ''); + const match = /^claude-([a-z]+)-(\d+)(?:-(\d+))?/u.exec(model); + if (match) return `${match[1]![0]!.toUpperCase()}${match[1]!.slice(1)} ${match[2]}${match[3] && match[3].length <= 2 ? `.${match[3]}` : ''}`; + return displayText(model).slice(0, 40); +} + +/** Cache reads as a share of all input the provider counted for this target (how much of the prompt was reused). */ +export function cacheShare(usage: UsageTotals | undefined): number | undefined { + if (!usage) return undefined; + let read = 0, total = 0; + for (const item of Object.values(usage.perModel)) { read += item.cacheRead; total += item.input + item.cacheRead + item.cacheWrite; } + return total > 0 ? read / total : undefined; +} + +export function formatTokens(value: number): string { + if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(value >= 10_000_000 ? 0 : 1).replace(/\.0$/u, '')}M`; + if (value >= 1000) return `${(value / 1000).toFixed(value >= 100_000 ? 0 : 1).replace(/\.0$/u, '')}k`; + return String(Math.round(value)); +} diff --git a/tests/agentControlPlane.test.ts b/tests/agentControlPlane.test.ts new file mode 100644 index 00000000..96243364 --- /dev/null +++ b/tests/agentControlPlane.test.ts @@ -0,0 +1,252 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync} from 'node:fs'; +import {tmpdir} from 'node:os'; +import {join} from 'node:path'; +import {ClaudeStream, parseCatalog, parseContextUsage, parseSuggestions, parseToolResult} from '../src/agents/sessions/claudeStream.js'; +import {claudeEvents} from '../src/agents/sessions/claudeAdapter.js'; +import {AgentSessions} from '../src/agents/sessions/manager.js'; +import {applyTelemetry, contextMeter, currentModel, emptyTelemetry, modelName, sameModel} from '../src/agents/telemetry.js'; + +/* Shapes below are the ones Claude Code 2.1.295 sent in the recorded control probe (scripts/probes/claude-controls.mjs). */ +const INITIALIZE = { + commands: [{name: 'model', description: 'Set the AI model', argumentHint: '', builtin: true}, {name: 'compact', description: 'Compact the conversation', argumentHint: '', builtin: true}, + {name: 'design', description: 'A plugin skill', argumentHint: 'consent | revoke'}, {name: 'bad name!', description: 'refused'}], + agents: [{name: 'Explore', description: 'Fast search'}, {name: 'general-purpose', description: 'General'}], + output_style: 'default', available_output_styles: ['default', 'Concise'], + models: [{value: 'default', resolvedModel: 'claude-opus-5-5', displayName: 'Default (recommended)', description: 'Opus 5.5', supportsEffort: true, supportedEffortLevels: ['low', 'medium', 'high', 'xhigh', 'max']}, + {value: 'sonnet', resolvedModel: 'claude-sonnet-5-5', displayName: 'Sonnet 5.5', description: 'Fast', supportsEffort: true, supportedEffortLevels: ['low', 'medium', 'high', 'xhigh', 'max']}, + {value: 'claude-haiku-4-5-20251001', resolvedModel: 'claude-haiku-4-5-20251001', displayName: 'Haiku 4.5', description: 'No effort'}], + account: {email: 'person@example.com', organization: 'Org', subscriptionType: 'Claude Pro', apiProvider: 'firstParty'}, +}; +const CONTEXT = {categories: [{name: 'System prompt', tokens: 2356, color: 'x', kind: 'used'}, {name: 'System tools', tokens: 1807, color: 'x', kind: 'used'}, {name: 'Messages', tokens: 785, color: 'x', kind: 'used'}, + {name: 'Autocompact buffer', tokens: 33000, color: 'x', kind: 'buffer'}, {name: 'Free space', tokens: 962052, color: 'x', kind: 'free'}, {name: 'Bogus', tokens: 1, kind: 'weird'}], +totalTokens: 4948, maxTokens: 1000000, rawMaxTokens: 1000000, percentage: 0, gridRows: [], model: 'claude-sonnet-5-5', memoryFiles: [{path: '/p/CLAUDE.md', type: 'Project', tokens: 812}], mcpTools: [], agents: [], +autoCompactThreshold: 967000, isAutoCompactEnabled: true, +messageBreakdown: {toolCallTokens: 125, toolResultTokens: 100, attachmentTokens: 0, assistantMessageTokens: 16, userMessageTokens: 151, redirectedContextTokens: 0, unattributedTokens: 0, + toolCallsByType: [{name: 'Edit', callTokens: 70, resultTokens: 71}, {name: 'Read', callTokens: 55, resultTokens: 29}], attachmentsByType: []}, +apiUsage: {input_tokens: 2, output_tokens: 5, cache_creation_input_tokens: 303, cache_read_input_tokens: 4643}}; + +test('initialize: the model catalog, commands and account plan, with refused shapes dropped', () => { + const catalog = parseCatalog(INITIALIZE, 5)!; + assert.deepEqual(catalog.models.map(model => [model.value, model.name, model.resolved, model.effortLevels?.length ?? 0]), + [['default', 'Default (recommended)', 'claude-opus-5-5', 5], ['sonnet', 'Sonnet 5.5', 'claude-sonnet-5-5', 5], ['claude-haiku-4-5-20251001', 'Haiku 4.5', 'claude-haiku-4-5-20251001', 0]]); + assert.deepEqual(catalog.commands.map(command => `${command.name}${command.builtin ? '*' : ''}`), ['model*', 'compact*', 'design'], 'an unsafe command name is not offered'); + assert.deepEqual(catalog.agents, ['Explore', 'general-purpose']); + assert.equal(catalog.account?.plan, 'Claude Pro'); + assert.equal(parseCatalog('nope', 1), undefined); +}); + +test('context usage: provider categories by kind, memory files and the per-tool message breakdown', () => { + const snapshot = parseContextUsage(CONTEXT, 'summary')!; + assert.deepEqual(snapshot.categories.map(category => category.kind), ['used', 'used', 'used', 'buffer', 'free'], 'unknown kinds are dropped, never guessed'); + assert.equal(snapshot.totalTokens, 4948); + assert.equal(snapshot.autoCompactThreshold, 967000); + assert.deepEqual(snapshot.memoryFiles, [{path: '/p/CLAUDE.md', type: 'Project', tokens: 812}]); + assert.deepEqual(snapshot.messages?.byTool, [{name: 'Edit', call: 70, result: 71}, {name: 'Read', call: 55, result: 29}]); + assert.equal(parseContextUsage({categories: []}, 'summary'), undefined); +}); + +test('tool results: provider patches, reads, commands, searches, plans and subagent reports; whole files are never kept', () => { + const edit = parseToolResult({filePath: '/p/fixture.txt', oldString: 'beta', newString: 'BETA', originalFile: 'alpha\nbeta\ngamma\nSECRET-WHOLE-FILE\n', replaceAll: false, userModified: false, + structuredPatch: [{oldStart: 1, oldLines: 3, newStart: 1, newLines: 3, lines: [' alpha', '-beta', '+BETA', ' gamma']}]})!; + assert.deepEqual(edit, {kind: 'patch', path: '/p/fixture.txt', hunks: [{oldStart: 1, oldLines: 3, newStart: 1, newLines: 3, lines: [' alpha', '-beta', '+BETA', ' gamma']}], added: 1, removed: 1, created: false, truncated: false}); + assert.ok(!JSON.stringify(edit).includes('SECRET-WHOLE-FILE')); + const created = parseToolResult({type: 'create', filePath: '/p/new.ts', content: 'a\nb\nc', structuredPatch: [], originalFile: null})!; + assert.deepEqual([created.kind, (created as {created: boolean}).created, (created as {added: number}).added], ['patch', true, 3]); + assert.deepEqual(parseToolResult({type: 'text', file: {filePath: '/p/a.ts', content: 'x', numLines: 10, startLine: 1, totalLines: 40}}), {kind: 'read', path: '/p/a.ts', lines: 10, start: 1, total: 40}); + const bash = parseToolResult({stdout: 'ok\n'.repeat(40000), stderr: '', interrupted: false})!; + assert.equal(bash.kind, 'bash'); + assert.ok((bash as {stdout: string}).stdout.length < 70 * 1024 && (bash as {truncated: boolean}).truncated, 'long output keeps its start and end, marked'); + assert.deepEqual(parseToolResult({mode: 'files_with_matches', numFiles: 2, filenames: ['a', 'b'], numMatches: 5}), {kind: 'search', files: 2, matches: 5, names: ['a', 'b']}); + assert.deepEqual(parseToolResult({oldTodos: [], newTodos: [{content: 'Write tests', status: 'in_progress', activeForm: 'Writing tests'}, {content: 'x', status: 'bogus'}]}), + {kind: 'todos', todos: [{content: 'Write tests', status: 'in_progress', activeForm: 'Writing tests'}]}); + assert.equal(parseToolResult({agentId: 'a1', content: [{type: 'text', text: 'Found 3 callers.'}], totalToolUseCount: 4, totalDurationMs: 9000, totalTokens: 1234})?.kind, 'agent'); + assert.equal(parseToolResult('text'), undefined); +}); + +test('permission suggestions: the provider offers in words, destinations named, unknown offers not shown', () => { + const suggestions = parseSuggestions([ + {type: 'addRules', rules: [{toolName: 'Bash', ruleContent: 'npm test:*'}], behavior: 'allow', destination: 'session'}, + {type: 'addRules', rules: [{toolName: 'Edit'}], behavior: 'allow', destination: 'userSettings'}, + {type: 'setMode', mode: 'acceptEdits', destination: 'session'}, + {type: 'addRules', rules: [{toolName: 'Bash'}], behavior: 'deny', destination: 'session'}, + {type: 'mystery', destination: 'session'}, + ]); + assert.deepEqual(suggestions.map(item => item.label), ['Allow Bash(npm test:*) for this session', 'Allow Edit for every project (user settings)', 'Switch to acceptEdits mode for this session']); + assert.equal(suggestions[0]!.update.destination, 'session', 'the offer is returned verbatim when chosen'); +}); + +test('stream: init, status, compaction, retries, rate limits, tasks, usage and partial text become telemetry', () => { + const stream = new ClaudeStream(() => 1000); + const init = stream.parse({type: 'system', subtype: 'init', model: 'claude-sonnet-5-5', permissionMode: 'acceptEdits', tools: ['Edit', 'Read'], mcp_servers: [{name: 'gh', status: 'connected'}], + slash_commands: ['compact', 'context'], skills: [], plugins: [{name: 'p', version: '1.0.0'}], output_style: 'default', claude_code_version: '2.1.295', capabilities: ['interrupt_receipt_v1']}); + assert.equal(init.telemetry[0]!.kind, 'runtime'); + assert.equal((init.telemetry[0] as {runtime: {permissionMode: string}}).runtime.permissionMode, 'acceptEdits'); + assert.equal((init.telemetry[0] as {runtime: {effort?: unknown}}).runtime.effort, undefined, 'headless init reports no effort: none is invented'); + const compact = stream.parse({type: 'system', subtype: 'compact_boundary', compact_metadata: {trigger: 'auto', pre_tokens: 182000, post_tokens: 31000}}); + assert.deepEqual(compact.events, [{kind: 'compacted', trigger: 'auto', preTokens: 182000, postTokens: 31000}]); + const limit = stream.parse({type: 'rate_limit_event', rate_limit_info: {status: 'allowed_warning', rateLimitType: 'seven_day', utilization: 0.5, resetsAt: 1_800_000_000}}); + assert.deepEqual(limit.telemetry, [{kind: 'rateLimit', type: 'seven_day', at: 1000, limit: {status: 'allowed_warning', utilization: 0.5, resetsAt: 1_800_000_000_000}}]); + const task = stream.parse({type: 'system', subtype: 'task_started', task_id: 'tk', description: 'Find callers', subagent_type: 'Explore', task_type: 'local_agent', tool_use_id: 'tu'}); + assert.equal((task.telemetry[0] as {task: {status: string}}).task.status, 'running'); + assert.deepEqual(stream.parse({type: 'system', subtype: 'task_started', task_id: 'amb', description: 'x', ambient: true}).telemetry, [], 'ambient housekeeping is not activity'); + const result = stream.parse({type: 'result', subtype: 'success', is_error: false, num_turns: 3, duration_ms: 5000, duration_api_ms: 4000, total_cost_usd: 0.018, + usage: {input_tokens: 6, output_tokens: 271, cache_read_input_tokens: 10899, cache_creation_input_tokens: 3146}, + modelUsage: {'claude-sonnet-5-5': {inputTokens: 6, outputTokens: 271, cacheReadInputTokens: 10899, cacheCreationInputTokens: 3146, costUSD: 0.0175, contextWindow: 1000000, maxOutputTokens: 128000}}}); + assert.deepEqual(result.telemetry.map(update => update.kind), ['usage', 'settled']); + // Streaming deltas are presentation; a subagent's stream never mixes into the main reply. + stream.parse({type: 'stream_event', parent_tool_use_id: null, event: {type: 'message_start'}}); + stream.parse({type: 'stream_event', parent_tool_use_id: null, event: {type: 'content_block_start', content_block: {type: 'text'}}}); + stream.parse({type: 'stream_event', parent_tool_use_id: null, event: {type: 'content_block_delta', delta: {type: 'text_delta', text: 'Hel'}}}); + assert.deepEqual(stream.parse({type: 'stream_event', parent_tool_use_id: null, event: {type: 'content_block_delta', delta: {type: 'text_delta', text: 'lo'}}}).partial, {text: 'Hello'}); + assert.equal(stream.parse({type: 'stream_event', parent_tool_use_id: 'sub', event: {type: 'content_block_delta', delta: {type: 'text_delta', text: 'X'}}}).partial, undefined); + assert.deepEqual(stream.parse({type: 'assistant', parent_tool_use_id: null, message: {content: [{type: 'text', text: 'Hello'}]}}).partial, {}, 'the completed message replaces the stream'); + // Files: reads when requested, edits from the provider's own patch; a plan from TodoWrite. + const tools = stream.parse({type: 'assistant', message: {content: [{type: 'tool_use', id: 'r', name: 'Read', input: {file_path: '/p/a.ts'}}, {type: 'tool_use', id: 'e', name: 'Edit', input: {file_path: '/p/a.ts'}}, + {type: 'tool_use', id: 'td', name: 'TodoWrite', input: {todos: [{content: 'Run tests', status: 'pending', activeForm: 'Running tests'}]}}]}}); + assert.deepEqual(tools.telemetry.map(update => update.kind), ['file', 'todos']); + const edited = stream.parse({type: 'user', message: {content: [{type: 'tool_result', tool_use_id: 'e', content: 'ok'}]}, tool_use_result: {filePath: '/p/a.ts', structuredPatch: [{oldStart: 1, oldLines: 1, newStart: 1, newLines: 2, lines: ['-a', '+b', '+c']}]}}); + assert.deepEqual(edited.telemetry.map(update => [update.kind, (update as {action?: string}).action, (update as {added?: number}).added]), [['file', 'edit', 2]]); + const denied = stream.parse({type: 'user', message: {content: [{type: 'tool_result', tool_use_id: 'e', is_error: true, content: 'denied'}]}, tool_use_result: {filePath: '/p/a.ts', structuredPatch: []}}); + assert.deepEqual(denied.telemetry, [], 'a failed edit changed nothing'); +}); + +test('telemetry: context meter prefers the provider breakdown, estimates from the newest turn, never from lifetime totals', () => { + const telemetry = emptyTelemetry(); + assert.equal(contextMeter(telemetry), undefined, 'nothing reported means nothing shown, never 0%'); + applyTelemetry(telemetry, {kind: 'usage', at: 10, usage: {perModel: {'claude-sonnet-5-5': {input: 900000, output: 50000, cacheRead: 5_000_000, cacheWrite: 1, contextWindow: 1_000_000}}, + lastTurn: {input: 6, output: 271, cacheRead: 10899, cacheWrite: 3146, model: 'claude-sonnet-5-5'}, turns: 3, durationMs: 1, apiMs: 1}}); + const estimate = contextMeter(telemetry)!; + assert.equal(estimate.source, 'estimate'); + assert.equal(estimate.used, 6 + 271 + 10899 + 3146, 'the newest request, not lifetime spend (which here exceeds the window)'); + assert.equal(estimate.max, 1_000_000); + applyTelemetry(telemetry, {kind: 'context', at: 20, snapshot: parseContextUsage(CONTEXT, 'summary')!}); + assert.deepEqual([contextMeter(telemetry)!.source, contextMeter(telemetry)!.used], ['provider', 4948]); + applyTelemetry(telemetry, {kind: 'usage', at: 30, usage: {perModel: {}, lastTurn: {input: 1, output: 1, cacheRead: 20000, cacheWrite: 0, model: 'x'}, turns: 4, durationMs: 1, apiMs: 1}}); + assert.equal(contextMeter(telemetry)!.source, 'estimate', 'a turn after the breakdown makes the breakdown stale'); + applyTelemetry(telemetry, {kind: 'compaction', compaction: {at: 40, trigger: 'manual', preTokens: 5000, postTokens: 900}}); + assert.equal(telemetry.context, undefined, 'compaction invalidates the breakdown'); +}); + +test('telemetry: requested model is pending until the provider reports it; aliases resolve through the catalog', () => { + const telemetry = emptyTelemetry(); + applyTelemetry(telemetry, {kind: 'catalog', catalog: parseCatalog(INITIALIZE, 1)!}); + applyTelemetry(telemetry, {kind: 'runtime', runtime: {model: 'claude-opus-5-5', tools: [], mcpServers: [], slashCommands: [], skills: [], plugins: [], capabilities: [], at: 2}}); + assert.deepEqual([currentModel(telemetry)!.value, currentModel(telemetry)!.source], ['claude-opus-5-5', 'provider']); + applyTelemetry(telemetry, {kind: 'requested', field: 'model', value: 'sonnet', at: 3}); + assert.deepEqual([currentModel(telemetry)!.value, currentModel(telemetry)!.source], ['sonnet', 'requested'], 'acknowledged, not yet reported'); + applyTelemetry(telemetry, {kind: 'runtime', runtime: {model: 'claude-sonnet-5-5', tools: [], mcpServers: [], slashCommands: [], skills: [], plugins: [], capabilities: [], at: 4}}); + assert.deepEqual([currentModel(telemetry)!.value, currentModel(telemetry)!.source], ['claude-sonnet-5-5', 'provider']); + assert.ok(sameModel('sonnet', 'claude-sonnet-5-5', telemetry)); + assert.equal(modelName('claude-sonnet-5-5', telemetry), 'Sonnet 5.5'); + assert.equal(modelName('claude-opus-5-5', telemetry), 'Opus 5.5', 'never "Default (recommended)" for a concrete model'); + assert.equal(modelName('claude-fable-5-1', undefined), 'Fable 5.1'); + assert.deepEqual(telemetry.modelHistory.map(entry => [entry.model, entry.source]), [['claude-opus-5-5', 'provider'], ['sonnet', 'requested'], ['claude-sonnet-5-5', 'provider']]); +}); + +test('claudeEvents: tool results carry structured results and subagent parents only when the provider sends them', () => { + const plain = claudeEvents(JSON.stringify({type: 'user', message: {content: [{type: 'tool_result', tool_use_id: 't', content: 'x'}]}})).events[0] as Record; + assert.deepEqual(Object.keys(plain).sort(), ['detail', 'id', 'kind', 'name', 'status']); + const rich = claudeEvents(JSON.stringify({type: 'user', parent_tool_use_id: 'task-1', message: {content: [{type: 'tool_result', tool_use_id: 't', content: 'x'}]}, + tool_use_result: {filePath: '/a', structuredPatch: [{oldStart: 1, oldLines: 1, newStart: 1, newLines: 1, lines: ['-a', '+b']}]}})).events[0] as {result?: {kind: string}; parent?: string}; + assert.equal(rich.result?.kind, 'patch'); + assert.equal(rich.parent, 'task-1'); + const approval = claudeEvents(JSON.stringify({type: 'control_request', request_id: 'r', request: {subtype: 'can_use_tool', tool_name: 'Bash', input: {command: 'npm test'}, tool_use_id: 'tu', + decision_reason: 'Command not in allow list', permission_suggestions: [{type: 'addRules', rules: [{toolName: 'Bash', ruleContent: 'npm test'}], behavior: 'allow', destination: 'session'}]}})).events[0] as Record; + assert.equal(approval.reason, 'Command not in allow list'); + assert.equal((approval.suggestions as Array<{label: string}>)[0]!.label, 'Allow Bash(npm test) for this session'); +}); + +// ---- A stand-in CLI speaking the documented control protocol ------------------------------------------------------ + +const root = mkdtempSync(join(tmpdir(), 'nmsh-control-')); +test.after(() => rmSync(root, {recursive: true, force: true})); +const HELP = 'Usage: claude [options]\n -p, --print\n --input-format "stream-json"\n --output-format "stream-json"\n --permission-prompts "host"\n -r, --resume [value]\n --session-id \n --include-partial-messages\n --effort \n'; +const fake = join(root, 'claude'); +writeFileSync(fake, `#!/usr/bin/env node +const fs = require('fs'); +fs.writeFileSync(${JSON.stringify(join(root, 'argv.json'))}, JSON.stringify(process.argv.slice(2))); +const out = message => process.stdout.write(JSON.stringify(message) + '\\n'); +let model = 'claude-opus-5-5', mode = 'default'; +let buffer = ''; +process.stdin.on('data', chunk => { + buffer += chunk; + let i; + while ((i = buffer.indexOf('\\n')) >= 0) { + const line = JSON.parse(buffer.slice(0, i)); buffer = buffer.slice(i + 1); + fs.appendFileSync(${JSON.stringify(join(root, 'stdin.jsonl'))}, JSON.stringify(line) + '\\n'); + const reply = (body) => out({type: 'control_response', response: {subtype: 'success', request_id: line.request_id, response: body}}); + const refuse = (error) => out({type: 'control_response', response: {subtype: 'error', request_id: line.request_id, error}}); + if (line.type === 'control_request') { + const r = line.request; + if (r.subtype === 'initialize') reply(${JSON.stringify(INITIALIZE)}); + else if (r.subtype === 'set_model') { model = r.model === 'sonnet' ? 'claude-sonnet-5-5' : r.model; reply(undefined); } + else if (r.subtype === 'apply_flag_settings') reply(undefined); + else if (r.subtype === 'set_permission_mode') { mode = r.mode; reply({mode}); } + else if (r.subtype === 'get_context_usage') reply(${JSON.stringify(CONTEXT)}); + else if (r.subtype === 'mcp_status') reply({mcpServers: [{name: 'gh', status: 'connected', tools: [{name: 'search'}]}]}); + else refuse('Unsupported control request'); + } + if (line.type === 'user') { + out({type: 'system', subtype: 'init', session_id: 's1', model, permissionMode: mode, tools: [], mcp_servers: [], slash_commands: [], capabilities: []}); + out({type: 'stream_event', parent_tool_use_id: null, event: {type: 'message_start'}}); + out({type: 'stream_event', parent_tool_use_id: null, event: {type: 'content_block_start', content_block: {type: 'text'}}}); + out({type: 'stream_event', parent_tool_use_id: null, event: {type: 'content_block_delta', delta: {type: 'text_delta', text: 'Done.'}}}); + out({type: 'assistant', message: {model, content: [{type: 'text', text: 'Done.'}]}}); + out({type: 'result', subtype: 'success', is_error: false, num_turns: 1, duration_ms: 10, duration_api_ms: 8, total_cost_usd: 0.01, + usage: {input_tokens: 5, output_tokens: 2, cache_read_input_tokens: 4000, cache_creation_input_tokens: 0}, + modelUsage: {[model]: {inputTokens: 5, outputTokens: 2, cacheReadInputTokens: 4000, cacheCreationInputTokens: 0, costUSD: 0.01, contextWindow: 1000000, maxOutputTokens: 128000}}}); + } + } +}); +process.stdin.on('end', () => process.exit(0)); +`); +chmodSync(fake, 0o755); +const until = async (check: () => boolean) => { for (let i = 0; i < 400 && !check(); i += 1) await new Promise(resolve => setTimeout(resolve, 10)); assert.ok(check(), 'condition reached'); }; + +test('controls: set_model, effort, permission mode, context and MCP are real acknowledged requests that update the target', async () => { + const sessions = new AgentSessions({resolve: name => name === 'claude' ? fake : undefined, scan: async () => [], now: () => Date.now(), claudeHelp: HELP}); + try { + const launched = sessions.launch('claude', root, {profile: {name: 'p', harness: 'claude', effort: 'high'}}); + assert.ok(launched.ok); + const session = launched.session; + await until(() => Boolean(session.telemetry?.catalog)); + const argv = JSON.parse(readFileSync(join(root, 'argv.json'), 'utf8')) as string[]; + assert.ok(argv.includes('--include-partial-messages'), 'streaming is requested when documented'); + assert.deepEqual(argv.slice(argv.indexOf('--effort'), argv.indexOf('--effort') + 2), ['--effort', 'high'], 'a profile effort is a launch flag, never a settings write'); + assert.equal(session.telemetry!.catalog!.models.length, 3); + assert.equal(session.telemetry!.requested.effort?.value, 'high'); + + const model = await sessions.setModel(session.id, 'sonnet'); + assert.deepEqual(model, {ok: true, value: undefined}); + assert.equal(session.telemetry!.requested.model?.value, 'sonnet'); + assert.ok(session.events.some(event => event.kind === 'control' && event.field === 'model'), 'the change is part of the conversation record'); + const effort = await sessions.setEffort(session.id, 'low'); + assert.ok(effort.ok); + assert.equal((await sessions.setEffort(session.id, 'turbo')).ok, false, 'only documented levels are sent'); + assert.ok((await sessions.setPermissionMode(session.id, 'acceptEdits')).ok); + assert.equal((await sessions.setPermissionMode(session.id, 'bypassPermissions')).ok, false, 'bypassing every check is never sent'); + const context = await sessions.contextUsage(session.id); + assert.ok(context.ok && context.value.totalTokens === 4948); + assert.equal(session.telemetry!.context?.source, 'provider'); + const mcp = await sessions.mcpStatus(session.id); + assert.ok(mcp.ok && mcp.value.length === 1); + const refused = await sessions.stopTask(session.id, 'none'); + assert.deepEqual(refused, {ok: false, reason: 'Unsupported control request'}, "the provider's refusal is reported, not hidden"); + + let partials = 0; + sessions.onPartial(() => { partials++; }); + sessions.send(session.id, 'go'); + await until(() => session.state === 'waiting'); + assert.ok(partials > 0, 'streaming text reached the host'); + assert.equal(session.partial, undefined, 'and was replaced by the completed message'); + assert.equal(session.telemetry!.runtime?.model, 'claude-sonnet-5-5', 'the next turn reports the requested model'); + assert.equal(session.telemetry!.runtime?.permissionMode, 'acceptEdits'); + assert.equal(session.telemetry!.usage?.costUsd, 0.01); + const sent = readFileSync(join(root, 'stdin.jsonl'), 'utf8').trim().split('\n').map(line => JSON.parse(line) as {type: string; request?: {subtype: string; settings?: unknown}}); + assert.deepEqual(sent.filter(line => line.type === 'control_request').map(line => line.request!.subtype), + ['initialize', 'set_model', 'apply_flag_settings', 'set_permission_mode', 'get_context_usage', 'mcp_status', 'stop_task']); + assert.deepEqual(sent.find(line => line.request?.subtype === 'apply_flag_settings')!.request!.settings, {effortLevel: 'low'}); + } finally { sessions.dispose(); } +}); From 711d6fe9ffbb515a9e53e267fa25f0b718aa1cb5 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:54:25 +0530 Subject: [PATCH 005/249] Agent results: read a result only as the tool that produced it; offers are exact and session-only An MCP tool returning a result shaped like an edit could have appeared as a diff and a file activity. Structured results are now bound to the built-in tool that produced them. Permission rule offers are rebuilt from exactly the fields shown, limited to this session, allow-only, and never switch to a mode that bypasses checks. --- src/agents/sessions/claudeAdapter.ts | 15 +++--- src/agents/sessions/claudeStream.ts | 75 +++++++++++++++++++++------- tests/agentControlPlane.test.ts | 43 +++++++++++----- 3 files changed, 95 insertions(+), 38 deletions(-) diff --git a/src/agents/sessions/claudeAdapter.ts b/src/agents/sessions/claudeAdapter.ts index 515506d1..a84e000b 100644 --- a/src/agents/sessions/claudeAdapter.ts +++ b/src/agents/sessions/claudeAdapter.ts @@ -103,14 +103,17 @@ export function resultFailure(message: Record): string | undefi type ClaudeLine = {events: AgentEvent[]; control?: {requestId: string; subtype: string; tool?: string; input?: unknown}; turnError?: string}; /** One stdout line → normalized events (unknown shapes produce nothing). */ -export function claudeEvents(line: string): ClaudeLine { +export function claudeEvents(line: string, toolName?: (id: string) => string | undefined): ClaudeLine { let message: unknown; try { message = JSON.parse(line); } catch { return {events: []}; } - return message && typeof message === 'object' && !Array.isArray(message) ? claudeMessageEvents(message as Record) : {events: []}; + return message && typeof message === 'object' && !Array.isArray(message) ? claudeMessageEvents(message as Record, toolName) : {events: []}; } -/** One parsed stream-json message → normalized events. */ -export function claudeMessageEvents(message: Record): ClaudeLine { +/** + * One parsed stream-json message → normalized events. `toolName` names the tool behind a tool_use id; without it a + * tool result carries no structured reading (a result's shape alone never proves which tool produced it). + */ +export function claudeMessageEvents(message: Record, toolName?: (id: string) => string | undefined): ClaudeLine { const type = message.type; const parent = typeof message.parent_tool_use_id === 'string' && message.parent_tool_use_id ? message.parent_tool_use_id.slice(0, 128) : undefined; if (type === 'system' && message.subtype === 'init') { @@ -137,11 +140,11 @@ export function claudeMessageEvents(message: Record): ClaudeLin const content = (message.message as {content?: unknown} | undefined)?.content; const events: AgentEvent[] = []; // One structured result per message: Claude Code sends each tool result as its own user message. - const result = parseToolResult(message.tool_use_result); for (const part of Array.isArray(content) ? content : []) { const block = part as {type?: unknown; tool_use_id?: unknown; is_error?: unknown; content?: unknown}; if (block.type === 'tool_result' && typeof block.tool_use_id === 'string') { const detail = textOf(block.content); + const result = parseToolResult(message.tool_use_result, toolName?.(block.tool_use_id)); events.push({kind: 'tool', id: block.tool_use_id, name: '', status: block.is_error === true ? 'failed' : 'finished', ...(detail ? {detail} : {}), ...(result && block.is_error !== true ? {result} : {}), ...(parent ? {parent} : {})}); } } @@ -302,7 +305,7 @@ export class ClaudeSession { } for (const update of parsedStream.telemetry) this.options.onTelemetry?.(update); if (parsedStream.partial) this.options.onPartial?.(parsedStream.partial); - const {events, control, turnError} = claudeMessageEvents(message); + const {events, control, turnError} = claudeMessageEvents(message, id => this.stream.toolName(id)); if (turnError) this.turnError = turnError; for (let event of events) { if (event.kind === 'model') { if (event.model === this.model) continue; this.model = event.model; } diff --git a/src/agents/sessions/claudeStream.ts b/src/agents/sessions/claudeStream.ts index de9654ff..dff29f14 100644 --- a/src/agents/sessions/claudeStream.ts +++ b/src/agents/sessions/claudeStream.ts @@ -117,8 +117,27 @@ function parsePatch(value: unknown): {hunks: PatchHunk[]; added: number; removed return {hunks, added, removed, truncated}; } -/** A tool's structured output by its shape (the documented *Output types). Contents of whole files are never kept. */ -export function parseToolResult(value: unknown): ToolResult | undefined { +/** Which built-in tool produces each structured result shape; any other tool (MCP, plugins, unknown) gets none. */ +const RESULT_TOOLS: Record> = { + patch: new Set(['Edit', 'MultiEdit', 'Write', 'NotebookEdit']), + read: new Set(['Read']), + bash: new Set(['Bash', 'BashOutput']), + search: new Set(['Grep', 'Glob']), + todos: new Set(['TodoWrite']), + agent: new Set(['Agent', 'Task']), +}; + +/** + * A built-in tool's structured output (the documented *Output types). The tool's name decides how a result may be + * read: a result shaped like an edit from an MCP tool is not an edit, so it never becomes a diff or a file activity. + * Contents of whole files are never kept. + */ +export function parseToolResult(value: unknown, tool: string | undefined): ToolResult | undefined { + const result = tool ? resultByShape(value) : undefined; + return result && RESULT_TOOLS[result.kind].has(tool!) ? result : undefined; +} + +function resultByShape(value: unknown): ToolResult | undefined { if (!isRecord(value)) return undefined; if (typeof value.filePath === 'string' && Array.isArray(value.structuredPatch)) { const patch = parsePatch(value.structuredPatch)!; @@ -164,21 +183,39 @@ export function parseTodos(value: unknown): TodoItem[] | undefined { return todos; } -/** Permission rule offers in plain words. Only typed fields are read; anything unrecognized is not offered. */ +/** Modes an offer may switch to: never one that bypasses permission checks. */ +const OFFERABLE_MODES = new Set(['default', 'acceptEdits', 'plan']); +const RULE_TEXT = /^[^\u0000-\u001f\u007f-\u009f\u2028-\u202e\u2066-\u2069\u200e\u200f\u061c]{0,160}$/u; + +/** + * Permission rule offers in plain words. An offer is actionable only when the words say everything it does: it is + * rebuilt from exactly the fields shown (never returned with fields the person did not see), it is scoped to this + * session (NMSh never writes Claude's settings files), it allows rather than denies, it never switches to a mode + * that bypasses checks, and it is small enough to show whole. Anything else is not offered. + */ export function parseSuggestions(value: unknown): PermissionSuggestion[] { if (!Array.isArray(value)) return []; - return value.slice(0, 8).flatMap(item => { - if (!isRecord(item) || typeof item.type !== 'string' || typeof item.destination !== 'string') return []; - const where = item.destination === 'session' ? 'for this session' : item.destination === 'localSettings' ? 'in this project (local settings)' - : item.destination === 'projectSettings' ? 'in this project (shared settings)' : item.destination === 'userSettings' ? 'for every project (user settings)' : undefined; - if (!where) return []; - let label: string | undefined; - if (item.type === 'addRules' && Array.isArray(item.rules) && item.behavior === 'allow') { - const rules = item.rules.flatMap(rule => isRecord(rule) && typeof rule.toolName === 'string' ? [`${rule.toolName}${typeof rule.ruleContent === 'string' && rule.ruleContent ? `(${rule.ruleContent})` : ''}`] : []); - if (rules.length) label = `Allow ${rules.join(', ').slice(0, 200)} ${where}`; - } else if (item.type === 'setMode' && typeof item.mode === 'string') label = `Switch to ${item.mode} mode ${where}`; - else if (item.type === 'addDirectories' && Array.isArray(item.directories)) label = `Allow access to ${strings(item.directories, 4, 512).join(', ')} ${where}`; - return label ? [{update: item, label, destination: item.destination}] : []; + return value.slice(0, 8).flatMap((item): PermissionSuggestion[] => { + if (!isRecord(item) || item.destination !== 'session') return []; + if (item.type === 'addRules' && item.behavior === 'allow' && Array.isArray(item.rules) && item.rules.length >= 1 && item.rules.length <= 3) { + const rules: Array<{toolName: string; ruleContent?: string}> = []; + for (const rule of item.rules) { + if (!isRecord(rule) || typeof rule.toolName !== 'string' || !/^[\w:.\-]{1,96}$/u.test(rule.toolName)) return []; + if (rule.ruleContent !== undefined && (typeof rule.ruleContent !== 'string' || !RULE_TEXT.test(rule.ruleContent))) return []; + rules.push({toolName: rule.toolName, ...(typeof rule.ruleContent === 'string' && rule.ruleContent ? {ruleContent: rule.ruleContent} : {})}); + } + const words = rules.map(rule => `${rule.toolName}${rule.ruleContent ? `(${rule.ruleContent})` : ''}`).join(', '); + return [{update: {type: 'addRules', rules, behavior: 'allow', destination: 'session'}, label: `Allow ${words} for this session`, destination: 'session'}]; + } + if (item.type === 'setMode' && typeof item.mode === 'string' && OFFERABLE_MODES.has(item.mode)) { + return [{update: {type: 'setMode', mode: item.mode, destination: 'session'}, label: `Switch to ${item.mode} mode for this session`, destination: 'session'}]; + } + if (item.type === 'addDirectories' && Array.isArray(item.directories) && item.directories.length >= 1 && item.directories.length <= 2 + && item.directories.every(directory => typeof directory === 'string' && directory.startsWith('/') && RULE_TEXT.test(directory))) { + const directories = item.directories as string[]; + return [{update: {type: 'addDirectories', directories, destination: 'session'}, label: `Allow access to ${directories.join(', ')} for this session`, destination: 'session'}]; + } + return []; }); } @@ -310,10 +347,10 @@ export class ClaudeStream { } if (content.some(part => isRecord(part) && part.type === 'text') && !parent) { this.partial = ''; out.partial = {}; } } else { - const result = parseToolResult(message.tool_use_result); for (const part of content) { if (!isRecord(part) || part.type !== 'tool_result' || typeof part.tool_use_id !== 'string') continue; const tool = this.tools.get(part.tool_use_id); + const result = parseToolResult(message.tool_use_result, tool?.name); if (result?.kind === 'patch' && part.is_error !== true) { out.telemetry.push({kind: 'file', path: result.path, action: tool?.name === 'Write' || result.created ? 'write' : 'edit', added: result.added, removed: result.removed, patch: result.hunks, at}); } @@ -324,9 +361,9 @@ export class ClaudeStream { return out; } - /** The structured result for a tool_result block in a user message, when the message carries one. */ - resultFor(message: Record): ToolResult | undefined { - return parseToolResult(message.tool_use_result); + /** The name of the tool a tool_use id belongs to, as the provider announced it. */ + toolName(id: string): string | undefined { + return this.tools.get(id)?.name; } private toolStarted(name: string, input: Record | undefined, at: number): TelemetryUpdate[] { diff --git a/tests/agentControlPlane.test.ts b/tests/agentControlPlane.test.ts index 96243364..f5ec3281 100644 --- a/tests/agentControlPlane.test.ts +++ b/tests/agentControlPlane.test.ts @@ -49,32 +49,49 @@ test('context usage: provider categories by kind, memory files and the per-tool test('tool results: provider patches, reads, commands, searches, plans and subagent reports; whole files are never kept', () => { const edit = parseToolResult({filePath: '/p/fixture.txt', oldString: 'beta', newString: 'BETA', originalFile: 'alpha\nbeta\ngamma\nSECRET-WHOLE-FILE\n', replaceAll: false, userModified: false, - structuredPatch: [{oldStart: 1, oldLines: 3, newStart: 1, newLines: 3, lines: [' alpha', '-beta', '+BETA', ' gamma']}]})!; + structuredPatch: [{oldStart: 1, oldLines: 3, newStart: 1, newLines: 3, lines: [' alpha', '-beta', '+BETA', ' gamma']}]}, 'Edit')!; assert.deepEqual(edit, {kind: 'patch', path: '/p/fixture.txt', hunks: [{oldStart: 1, oldLines: 3, newStart: 1, newLines: 3, lines: [' alpha', '-beta', '+BETA', ' gamma']}], added: 1, removed: 1, created: false, truncated: false}); assert.ok(!JSON.stringify(edit).includes('SECRET-WHOLE-FILE')); - const created = parseToolResult({type: 'create', filePath: '/p/new.ts', content: 'a\nb\nc', structuredPatch: [], originalFile: null})!; + const created = parseToolResult({type: 'create', filePath: '/p/new.ts', content: 'a\nb\nc', structuredPatch: [], originalFile: null}, 'Write')!; assert.deepEqual([created.kind, (created as {created: boolean}).created, (created as {added: number}).added], ['patch', true, 3]); - assert.deepEqual(parseToolResult({type: 'text', file: {filePath: '/p/a.ts', content: 'x', numLines: 10, startLine: 1, totalLines: 40}}), {kind: 'read', path: '/p/a.ts', lines: 10, start: 1, total: 40}); - const bash = parseToolResult({stdout: 'ok\n'.repeat(40000), stderr: '', interrupted: false})!; + assert.deepEqual(parseToolResult({type: 'text', file: {filePath: '/p/a.ts', content: 'x', numLines: 10, startLine: 1, totalLines: 40}}, 'Read'), {kind: 'read', path: '/p/a.ts', lines: 10, start: 1, total: 40}); + const bash = parseToolResult({stdout: 'ok\n'.repeat(40000), stderr: '', interrupted: false}, 'Bash')!; assert.equal(bash.kind, 'bash'); assert.ok((bash as {stdout: string}).stdout.length < 70 * 1024 && (bash as {truncated: boolean}).truncated, 'long output keeps its start and end, marked'); - assert.deepEqual(parseToolResult({mode: 'files_with_matches', numFiles: 2, filenames: ['a', 'b'], numMatches: 5}), {kind: 'search', files: 2, matches: 5, names: ['a', 'b']}); - assert.deepEqual(parseToolResult({oldTodos: [], newTodos: [{content: 'Write tests', status: 'in_progress', activeForm: 'Writing tests'}, {content: 'x', status: 'bogus'}]}), + assert.deepEqual(parseToolResult({mode: 'files_with_matches', numFiles: 2, filenames: ['a', 'b'], numMatches: 5}, 'Grep'), {kind: 'search', files: 2, matches: 5, names: ['a', 'b']}); + assert.deepEqual(parseToolResult({oldTodos: [], newTodos: [{content: 'Write tests', status: 'in_progress', activeForm: 'Writing tests'}, {content: 'x', status: 'bogus'}]}, 'TodoWrite'), {kind: 'todos', todos: [{content: 'Write tests', status: 'in_progress', activeForm: 'Writing tests'}]}); - assert.equal(parseToolResult({agentId: 'a1', content: [{type: 'text', text: 'Found 3 callers.'}], totalToolUseCount: 4, totalDurationMs: 9000, totalTokens: 1234})?.kind, 'agent'); - assert.equal(parseToolResult('text'), undefined); + assert.equal(parseToolResult({agentId: 'a1', content: [{type: 'text', text: 'Found 3 callers.'}], totalToolUseCount: 4, totalDurationMs: 9000, totalTokens: 1234}, 'Agent')?.kind, 'agent'); + assert.equal(parseToolResult('text', 'Read'), undefined); }); -test('permission suggestions: the provider offers in words, destinations named, unknown offers not shown', () => { +test('security: a result is read only as the tool that produced it; an MCP tool cannot pose as an edit', () => { + const spoof = {filePath: '/etc/passwd', structuredPatch: [{oldStart: 1, oldLines: 1, newStart: 1, newLines: 1, lines: ['-root', '+pwned']}]}; + assert.equal(parseToolResult(spoof, 'mcp__evil__lookup'), undefined); + assert.equal(parseToolResult(spoof, undefined), undefined, 'an unknown tool gets no structured reading'); + assert.equal(parseToolResult(spoof, 'Read'), undefined, 'the shape must match the tool'); + const stream = new ClaudeStream(() => 1); + stream.parse({type: 'assistant', message: {content: [{type: 'tool_use', id: 'm', name: 'mcp__evil__lookup', input: {q: 'x'}}]}}); + assert.deepEqual(stream.parse({type: 'user', message: {content: [{type: 'tool_result', tool_use_id: 'm', content: 'ok'}]}, tool_use_result: spoof}).telemetry, [], 'no file activity is invented'); + const event = claudeEvents(JSON.stringify({type: 'user', message: {content: [{type: 'tool_result', tool_use_id: 'm', content: 'ok'}]}, tool_use_result: spoof}), () => 'mcp__evil__lookup').events[0] as {result?: unknown}; + assert.equal(event.result, undefined); +}); + +test('permission suggestions: only session offers whose words say everything they do', () => { const suggestions = parseSuggestions([ - {type: 'addRules', rules: [{toolName: 'Bash', ruleContent: 'npm test:*'}], behavior: 'allow', destination: 'session'}, + {type: 'addRules', rules: [{toolName: 'Bash', ruleContent: 'npm test:*', extra: 'hidden'}], behavior: 'allow', destination: 'session', sneaky: true}, {type: 'addRules', rules: [{toolName: 'Edit'}], behavior: 'allow', destination: 'userSettings'}, {type: 'setMode', mode: 'acceptEdits', destination: 'session'}, + {type: 'setMode', mode: 'bypassPermissions', destination: 'session'}, {type: 'addRules', rules: [{toolName: 'Bash'}], behavior: 'deny', destination: 'session'}, + {type: 'addRules', rules: [{toolName: 'A'}, {toolName: 'B'}, {toolName: 'C'}, {toolName: 'D'}], behavior: 'allow', destination: 'session'}, + {type: 'addRules', rules: [{toolName: 'Bash', ruleContent: 'rm -rf /\u202e'}], behavior: 'allow', destination: 'session'}, + {type: 'addDirectories', directories: ['/a', '/b', '/c'], destination: 'session'}, {type: 'mystery', destination: 'session'}, ]); - assert.deepEqual(suggestions.map(item => item.label), ['Allow Bash(npm test:*) for this session', 'Allow Edit for every project (user settings)', 'Switch to acceptEdits mode for this session']); - assert.equal(suggestions[0]!.update.destination, 'session', 'the offer is returned verbatim when chosen'); + assert.deepEqual(suggestions.map(item => item.label), ['Allow Bash(npm test:*) for this session', 'Switch to acceptEdits mode for this session']); + assert.deepEqual(suggestions[0]!.update, {type: 'addRules', rules: [{toolName: 'Bash', ruleContent: 'npm test:*'}], behavior: 'allow', destination: 'session'}, + 'the update is rebuilt from the shown fields only'); }); test('stream: init, status, compaction, retries, rate limits, tasks, usage and partial text become telemetry', () => { @@ -149,7 +166,7 @@ test('claudeEvents: tool results carry structured results and subagent parents o const plain = claudeEvents(JSON.stringify({type: 'user', message: {content: [{type: 'tool_result', tool_use_id: 't', content: 'x'}]}})).events[0] as Record; assert.deepEqual(Object.keys(plain).sort(), ['detail', 'id', 'kind', 'name', 'status']); const rich = claudeEvents(JSON.stringify({type: 'user', parent_tool_use_id: 'task-1', message: {content: [{type: 'tool_result', tool_use_id: 't', content: 'x'}]}, - tool_use_result: {filePath: '/a', structuredPatch: [{oldStart: 1, oldLines: 1, newStart: 1, newLines: 1, lines: ['-a', '+b']}]}})).events[0] as {result?: {kind: string}; parent?: string}; + tool_use_result: {filePath: '/a', structuredPatch: [{oldStart: 1, oldLines: 1, newStart: 1, newLines: 1, lines: ['-a', '+b']}]}}), () => 'Edit').events[0] as {result?: {kind: string}; parent?: string}; assert.equal(rich.result?.kind, 'patch'); assert.equal(rich.parent, 'task-1'); const approval = claudeEvents(JSON.stringify({type: 'control_request', request_id: 'r', request: {subtype: 'can_use_tool', tool_name: 'Bash', input: {command: 'npm test'}, tool_use_id: 'tu', From e01e66b454d9fab64bf98791135fe6fd77fe09b2 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:40:19 +0530 Subject: [PATCH 006/249] Managed Claude: native /model and /effort pickers In a managed Claude composer, /model and /effort open NMSh's own picker in the composer's place instead of going to Claude's prompt. Rows are what Claude published: its model catalog, and only the effort levels the running model supports (none for a model without levels, which the picker says). Up/Down choose, typing filters, Enter applies through set_model or apply_flag_settings, Esc backs out. /model sonnet and /effort high apply directly on an exact match; a leading space sends text verbatim. Nothing is claimed without evidence: a model change reads "requested" until Claude reports it; effort is shown as acknowledged, set at launch, read from the account's settings, or the model default (unreported) - never invented. The header shows model and effort with that provenance. A refusal keeps the picker open with the reason. --- src/agents/claudeSettings.ts | 24 ++++ src/agents/input/controller.ts | 7 +- src/agents/input/pickers.ts | 104 ++++++++++++++++ src/agents/input/surface.ts | 59 +++++++++ src/agents/sessions/AgentViews.ts | 2 + src/agents/sessions/manager.ts | 2 +- src/agents/telemetry.ts | 32 ++++- src/agents/transcript/view.ts | 52 +++++++- src/app/TerminalApp.ts | 19 ++- tests/agentPickers.test.ts | 200 ++++++++++++++++++++++++++++++ 10 files changed, 490 insertions(+), 11 deletions(-) create mode 100644 src/agents/claudeSettings.ts create mode 100644 src/agents/input/pickers.ts create mode 100644 tests/agentPickers.test.ts diff --git a/src/agents/claudeSettings.ts b/src/agents/claudeSettings.ts new file mode 100644 index 00000000..82b69342 --- /dev/null +++ b/src/agents/claudeSettings.ts @@ -0,0 +1,24 @@ +import {readFile} from 'node:fs/promises'; +import {homedir} from 'node:os'; +import {join} from 'node:path'; +import {EFFORT_LEVELS} from './sessions/claudeAdapter.js'; + +/** + * The effort level Claude Code would start with for a launch identity and directory, read passively from the settings + * files Claude documents (user, then project, then local; later wins). Only `effortLevel` is read; nothing is written. + * Managed (organization) settings and environment overrides are not consulted, so the result is labelled "from Claude + * settings", never presented as the level in effect. + */ +export async function settingsEffort(configDir: string | undefined, cwd: string): Promise { + const user = join(configDir ?? process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'), 'settings.json'); + let found: string | undefined; + for (const path of [user, join(cwd, '.claude', 'settings.json'), join(cwd, '.claude', 'settings.local.json')]) { + try { + const text = await readFile(path, 'utf8'); + if (text.length > 1024 * 1024) continue; + const value = (JSON.parse(text) as {effortLevel?: unknown}).effortLevel; + if (typeof value === 'string' && (EFFORT_LEVELS as readonly string[]).includes(value)) found = value; + } catch { /* absent or unreadable: no fact */ } + } + return found; +} diff --git a/src/agents/input/controller.ts b/src/agents/input/controller.ts index a60f73e4..513533ab 100644 --- a/src/agents/input/controller.ts +++ b/src/agents/input/controller.ts @@ -1,14 +1,15 @@ import {CommandEditor} from '../../input/CommandEditor.js'; import {applyEditingKey} from '../../input/editingKeys.js'; import type {Key} from '../../terminal/keys.js'; +import type {PickerState} from './pickers.js'; -export type InputOwner = 'COMPOSER' | 'TRANSCRIPT' | 'PANEL' | 'SEARCH' | 'CHOICE' | 'APPROVAL' | 'AGENT_MESSAGE' | 'PASSTHROUGH'; +export type InputOwner = 'COMPOSER' | 'TRANSCRIPT' | 'PANEL' | 'SEARCH' | 'CHOICE' | 'APPROVAL' | 'AGENT_MESSAGE' | 'PASSTHROUGH' | 'PICKER'; export type AgentAction = {kind: 'FocusTranscript' | 'FocusApproval' | 'Back' | 'ZoomIn' | 'ZoomOut' | 'Approve' | 'Deny' | 'SendMessage' | 'Interrupt' | 'Choose' | 'CopyVisible' | 'CopyFull'} | {kind: 'Navigate'; direction: number} | {kind: 'SelectDetailDepth'; depth: number} | {kind: 'ScrollTranscript'; rows: number} | {kind: 'EditMessage'; key: Key}; /** Context-specific bindings only. Controllers consume actions, not literal sequences. */ export function agentKeyAction(key: Key, owner: InputOwner): AgentAction | undefined { - if (owner === 'COMPOSER' || owner === 'PASSTHROUGH' || owner === 'PANEL' || owner === 'SEARCH') return undefined; + if (owner === 'COMPOSER' || owner === 'PASSTHROUGH' || owner === 'PANEL' || owner === 'SEARCH' || owner === 'PICKER') return undefined; if (key.kind === 'escape') return {kind: 'Back'}; if (owner === 'APPROVAL') { if (key.kind === 'toggleDetails') return {kind: 'Approve'}; @@ -54,6 +55,8 @@ export class AgentInputController { choiceAnswers: Record = {}; choiceToggled = new Set(); readonly choiceEditor = new CommandEditor(); + /** NMSh's own /model or /effort picker, while it owns the keys (owner PICKER). */ + picker?: PickerState; /** The draft's wrap width and first-row prefix as the view last drew them, so vertical moves follow the screen. */ composerColumns = 80; composerPrefix?: string; diff --git a/src/agents/input/pickers.ts b/src/agents/input/pickers.ts new file mode 100644 index 00000000..53653477 --- /dev/null +++ b/src/agents/input/pickers.ts @@ -0,0 +1,104 @@ +import type {Key} from '../../terminal/keys.js'; +import {displayText} from '../transcript/projection.js'; +import {currentModel, effortStatus, modelName, modelOption, sameModel, type AgentTelemetry} from '../telemetry.js'; + +/** + * NMSh's own /model and /effort pickers for a managed target. Rows come only from what the provider published (its + * model catalog and each model's supported effort levels); applying one is a real control request whose result the + * host reports. Nothing here is forwarded to the provider's prompt, and nothing claims a change the provider did not + * acknowledge. + */ +export type PickerKind = 'model' | 'effort'; + +export interface PickerRow { + /** What is sent: a model value, an effort level, or null for the model's default effort. */ + value: string | null; + label: string; + detail?: string; + current?: boolean; + isDefault?: boolean; +} + +export interface PickerState { + kind: PickerKind; + rows: PickerRow[]; + selected: number; + query: string; + /** The current value in words, with where it comes from. */ + status: string; + /** When the change takes effect, said before it is made. */ + timing: string; + busy?: boolean; + message?: string; +} + +/** Why a picker cannot open, in words; or the picker. */ +export type PickerOpen = {ok: true; picker: PickerState} | {ok: false; reason: string}; + +export function openModelPicker(telemetry: AgentTelemetry | undefined): PickerOpen { + const models = telemetry?.catalog?.models ?? []; + if (!telemetry || !models.length) return {ok: false, reason: 'Claude has not published its model list for this target yet. Try again once it has started.'}; + const current = currentModel(telemetry); + const exact = current ? models.findIndex(item => item.value === current.value && item.value !== 'default') : -1; + // One row is "current": the exact value asked for, else the first alias that resolves to the running model. + const currentIndex = exact >= 0 ? exact : current ? models.findIndex(item => item.value !== 'default' && sameModel(item.value, current.value, telemetry)) : -1; + const rows = models.slice(0, 32).map((item, index): PickerRow => ({value: item.value, label: displayText(item.name).slice(0, 60), + ...(item.description ? {detail: displayText(item.description).slice(0, 160)} : {}), + ...(index === currentIndex ? {current: true} : {}), ...(item.value === 'default' ? {isDefault: true} : {})})); + const status = !current ? 'Model not reported yet' + : current.source === 'provider' ? `Running ${modelName(current.value, telemetry)} · reported by Claude` + : `${modelName(current.value, telemetry)} requested · Claude confirms it on its next model call`; + return {ok: true, picker: {kind: 'model', rows, selected: Math.max(0, currentIndex), query: '', status, timing: 'Applies from Claude\'s next model call, mid-turn included; the conversation continues.'}}; +} + +export function openEffortPicker(telemetry: AgentTelemetry | undefined, settingsEffort?: string): PickerOpen { + if (!telemetry?.catalog?.models.length) return {ok: false, reason: 'Claude has not published its models and their effort levels for this target yet.'}; + const model = currentModel(telemetry)?.value; + const option = modelOption(telemetry, model); + if (!option) return {ok: false, reason: 'The running model is not reported yet, so its effort levels are unknown. Send a message first.'}; + const levels = option.effortLevels ?? []; + if (!levels.length) return {ok: false, reason: `${displayText(option.name)} has no effort levels to choose from.`}; + const status = effortStatus(telemetry, settingsEffort); + const currentValue = status.state === 'default' ? null : status.value; + const rows: PickerRow[] = [{value: null, label: 'Model default', detail: `Clears NMSh's choice; ${displayText(option.name)} uses its own default level`, isDefault: true, ...(currentValue === null ? {current: true} : {})}, + ...levels.slice(0, 8).map(level => ({value: level, label: level, ...(level === currentValue ? {current: true} : {}), ...(level === 'max' ? {detail: 'Highest effort; uses the most tokens'} : {})}))]; + return {ok: true, picker: {kind: 'effort', rows, selected: Math.max(0, rows.findIndex(row => row.current)), query: '', + status: `Effort: ${status.words}`, timing: 'Applies from the next turn, for this session only; Claude\'s settings files are not changed.'}}; +} + +/** Rows matching the typed filter (name, value or description). */ +export function visibleRows(picker: PickerState): PickerRow[] { + const query = picker.query.toLowerCase(); + return query ? picker.rows.filter(row => `${row.label} ${row.value ?? ''} ${row.detail ?? ''}`.toLowerCase().includes(query)) : picker.rows; +} + +/** A typed argument (`/model sonnet`, `/effort high`, `/effort default`) as one row, only on an exact match. */ +export function pickerArgument(picker: PickerState, argument: string): PickerRow | undefined { + const wanted = argument.trim().toLowerCase(); + if (!wanted) return undefined; + if (picker.kind === 'effort' && (wanted === 'default' || wanted === 'auto')) return picker.rows.find(row => row.value === null); + return picker.rows.find(row => row.value?.toLowerCase() === wanted || row.label.toLowerCase() === wanted); +} + +/** One key in the picker: move, filter, apply or close. Typing filters; nothing applies until Enter. */ +export function pickerKey(picker: PickerState, key: Key): 'apply' | 'close' | undefined { + if (picker.busy) return key.kind === 'escape' || key.kind === 'interrupt' ? 'close' : undefined; + const rows = visibleRows(picker); + if (key.kind === 'escape' || key.kind === 'interrupt') { + if (picker.query) { picker.query = ''; picker.selected = 0; return undefined; } + return 'close'; + } + if (key.kind === 'up' || key.kind === 'down') { picker.selected = Math.max(0, Math.min(rows.length - 1, picker.selected + (key.kind === 'up' ? -1 : 1))); return undefined; } + if (key.kind === 'pageUp' || key.kind === 'pageDown') { picker.selected = key.kind === 'pageUp' ? 0 : Math.max(0, rows.length - 1); return undefined; } + if (key.kind === 'enter') return rows[picker.selected] ? 'apply' : undefined; + if (key.kind === 'backspace') { picker.query = [...picker.query].slice(0, -1).join(''); picker.selected = 0; return undefined; } + if ((key.kind === 'text' || key.kind === 'paste') && !/[\u0000-\u001f]/u.test(key.value)) { + picker.query = (picker.query + key.value).slice(0, 40); picker.selected = 0; picker.message = undefined; return undefined; + } + return undefined; +} + +/** The row Enter would apply. */ +export function selectedRow(picker: PickerState): PickerRow | undefined { + return visibleRows(picker)[picker.selected]; +} diff --git a/src/agents/input/surface.ts b/src/agents/input/surface.ts index 985d53f9..16890b8e 100644 --- a/src/agents/input/surface.ts +++ b/src/agents/input/surface.ts @@ -4,6 +4,8 @@ import type {AgentViewState} from '../sessions/AgentViews.js'; import {AgentInputController, agentKeyAction} from './controller.js'; import {applyEditingKey} from '../../input/editingKeys.js'; import {projectObject} from '../transcript/projection.js'; +import {pickerArgument, pickerKey, selectedRow, type PickerKind, type PickerOpen, type PickerRow, type PickerState} from './pickers.js'; +import type {ControlResult} from '../sessions/claudeAdapter.js'; export interface AgentSurfaceHost { shellEmpty: boolean; @@ -13,11 +15,54 @@ export interface AgentSurfaceHost { copy(text: string): void; copyReply(session: AgentSession, index: number): void; choose?(id: string, requestId: string, answers: Record): boolean; + /** NMSh's own pickers: built from what the provider published for this target; absent where unsupported. */ + openPicker?(session: AgentSession, kind: PickerKind): PickerOpen; + /** Apply one picker row through the provider's control channel; the host re-renders when it settles. */ + applyPicker?(session: AgentSession, picker: PickerState, row: PickerRow): void; +} + +/** `/model`, `/model sonnet`, `/effort`, `/effort high`: NMSh's own commands in a managed agent composer. A leading space sends the text to the agent verbatim. */ +export function pickerCommand(text: string): {kind: PickerKind; argument: string} | undefined { + const match = /^\/(model|effort)(?:[ \t]+(\S{1,96}))?[ \t]*$/u.exec(text); + return match ? {kind: match[1] as PickerKind, argument: match[2] ?? ''} : undefined; +} + +/** + * Apply a picker choice and say what happened: the provider's acknowledgement (and when it takes effect), or its + * refusal with the picker kept open. Never reports a change the provider did not acknowledge. + */ +export async function applyPickerChoice(view: AgentViewState, picker: PickerState, row: PickerRow, ops: { + setModel(model: string | undefined): Promise; setEffort(level: string | null): Promise; render(): void; +}): Promise { + const c = view.controller!; + picker.busy = true; picker.message = undefined; ops.render(); + const result = picker.kind === 'model' ? await ops.setModel(row.value ?? undefined) : await ops.setEffort(row.value); + picker.busy = false; + if (!result.ok) { + // In the open picker the refusal stays beside the rows; a direct `/model x` reports it on the conversation. + if (c.owner === 'PICKER' && c.picker === picker) picker.message = `Not changed: ${result.reason}`; + else { view.message = `${picker.kind === 'model' ? 'Model' : 'Effort'} not changed: ${result.reason}`; if (c.picker === picker) c.picker = undefined; } + ops.render(); return false; + } + if (c.picker === picker) { c.picker = undefined; c.owner = 'AGENT_MESSAGE'; } + view.message = picker.kind === 'model' + ? `Model → ${row.label}. Claude acknowledged; it applies from the next model call and is confirmed when Claude reports it.` + : row.value === null ? 'Effort → model default. Claude acknowledged; applies from the next turn.' : `Effort → ${row.value}. Claude acknowledged; applies from the next turn (this session only).`; + ops.render(); + return true; } /** Adapter operations are supplied by the host; this controller is never a portable API. */ export function handleAgentInput(view: AgentViewState, session: AgentSession, key: Key, host: AgentSurfaceHost): boolean { const c = view.controller ??= new AgentInputController(); if (c.editor.text !== view.input) c.editor.replaceText(view.input); + if (c.owner === 'PICKER') { + const picker = c.picker; + if (!picker || session.level !== 'managed') { c.picker = undefined; c.owner = 'AGENT_MESSAGE'; return false; } + const outcome = pickerKey(picker, key); + if (outcome === 'close') { c.picker = undefined; c.owner = 'AGENT_MESSAGE'; } + else if (outcome === 'apply') { const row = selectedRow(picker); if (row) host.applyPicker?.(session, picker, row); } + return false; + } const action = agentKeyAction(key, c.owner); if (!action) return false; if (session.level !== 'managed') { @@ -65,6 +110,20 @@ export function handleAgentInput(view: AgentViewState, session: AgentSession, ke else view.message = 'No pending permission request.'; return false; } + const command = pickerCommand(text); + if (command) { + const opened = host.openPicker?.(session, command.kind) ?? {ok: false as const, reason: `/${command.kind} is not available for this target.`}; + c.editor.clear(); view.input = ''; + if (!opened.ok) { view.message = opened.reason; return false; } + const picker = opened.picker; + if (command.argument) { + const row = pickerArgument(picker, command.argument); + if (row) { c.picker = picker; host.applyPicker?.(session, picker, row); return false; } + picker.message = `No ${command.kind === 'model' ? 'model' : 'effort level'} named "${command.argument}"; choose one below.`; + } + c.picker = picker; c.owner = 'PICKER'; + return false; + } const copy = /^\/copy(?:\s+(\d+))?\s*$/u.exec(text.trim()); if (copy) {host.copyReply(session, Number(copy[1] ?? 1)); c.editor.clear();} else if (session.level !== 'managed' || !host.send(session.id, text)) view.message = 'This target is not accepting messages.'; diff --git a/src/agents/sessions/AgentViews.ts b/src/agents/sessions/AgentViews.ts index 96680422..4e93992f 100644 --- a/src/agents/sessions/AgentViews.ts +++ b/src/agents/sessions/AgentViews.ts @@ -197,6 +197,8 @@ export interface AgentViewMeta { configuredModel?: string; /** The shell's branch, when the target shares the shell's directory. */ branch?: string; + /** The effort level in the launch profile's Claude settings file, read passively by the host; labelled as such. */ + settingsEffort?: string; } export function renderAgentView(session: AgentSession, state: AgentViewState, columns: number, height: number, now: number, meta: AgentViewMeta = {}, options: AgentViewOptions = {}): string[] { diff --git a/src/agents/sessions/manager.ts b/src/agents/sessions/manager.ts index 6e7ae41c..f3d2d570 100644 --- a/src/agents/sessions/manager.ts +++ b/src/agents/sessions/manager.ts @@ -131,7 +131,7 @@ export class AgentSessions { session.partial = partial.thinking ? {text: '', at: now, thinking: true} : partial.text === undefined ? undefined : {text: partial.text, at: now}; this.emitPartial(session); }}, this.env.claudeHelp); - if (profile?.effort) applyTelemetry(session.telemetry, {kind: 'requested', field: 'effort', value: profile.effort, at: this.env.now()}); + if (profile?.effort && control.capabilities.effortFlag) applyTelemetry(session.telemetry, {kind: 'requested', field: 'effort', value: profile.effort, at: this.env.now(), evidence: 'launch flag'}); const started = control.start(); if (!started.ok) return started; session.pid = control.pid; diff --git a/src/agents/telemetry.ts b/src/agents/telemetry.ts index d90eb752..5f777cc9 100644 --- a/src/agents/telemetry.ts +++ b/src/agents/telemetry.ts @@ -214,7 +214,7 @@ export type TelemetryUpdate = | {kind: 'task'; task: Partial & {id: string}; at: number} | {kind: 'todos'; todos: TodoItem[]; at: number} | {kind: 'file'; path: string; action: 'read' | 'edit' | 'write'; added?: number; removed?: number; patch?: PatchHunk[]; at: number} - | {kind: 'requested'; field: 'model' | 'effort' | 'permissionMode'; value: string | null; at: number} + | {kind: 'requested'; field: 'model' | 'effort' | 'permissionMode'; value: string | null; at: number; evidence?: string} | {kind: 'settled'; at: number}; const MAX_FILES = 500; @@ -272,7 +272,7 @@ export function applyTelemetry(telemetry: AgentTelemetry, update: TelemetryUpdat break; } case 'requested': { - const evidence = update.field === 'model' ? 'set_model acknowledged' : update.field === 'effort' ? 'apply_flag_settings acknowledged' : 'set_permission_mode acknowledged'; + const evidence = update.evidence ?? (update.field === 'model' ? 'set_model acknowledged' : update.field === 'effort' ? 'apply_flag_settings acknowledged' : 'set_permission_mode acknowledged'); if (update.field === 'model' && update.value) pushHistory(telemetry, update.value, 'requested', update.at); (telemetry.requested as Record>)[update.field] = {value: update.value, source: 'requested', at: update.at, evidence}; break; @@ -359,3 +359,31 @@ export function formatTokens(value: number): string { if (value >= 1000) return `${(value / 1000).toFixed(value >= 100_000 ? 0 : 1).replace(/\.0$/u, '')}k`; return String(Math.round(value)); } + +/** The catalog entry for the model a target runs (or was asked to run): its name, aliases and effort levels. */ +export function modelOption(telemetry: AgentTelemetry | undefined, model: string | undefined): ModelOption | undefined { + if (!telemetry?.catalog || !model) return undefined; + const models = telemetry.catalog.models; + return models.find(item => item.value === model && item.value !== 'default') ?? models.find(item => item.resolved === model && item.value !== 'default') + ?? models.find(item => item.value === model) ?? models.find(item => item.resolved === model); +} + +export type EffortState = 'acknowledged' | 'launch' | 'settings' | 'default' | 'unavailable' | 'unknown'; + +/** + * What is known about a target's effort, honestly: headless Claude Code never reports the level in effect, so the + * best evidence is NMSh's own acknowledged request, then the launch flag, then the account's settings file. A model + * without effort levels has none to set; with nothing configured, the model's default applies (its level unreported). + */ +export function effortStatus(telemetry: AgentTelemetry | undefined, settingsEffort?: string): {value?: string; state: EffortState; words: string} { + if (!telemetry) return {state: 'unknown', words: 'unknown'}; + const option = modelOption(telemetry, currentModel(telemetry)?.value); + if (option && !option.effortLevels?.length) return {state: 'unavailable', words: 'this model has no effort levels'}; + const requested = telemetry.requested.effort; + if (requested) { + if (requested.evidence === 'launch flag') return {value: requested.value ?? undefined, state: 'launch', words: `${requested.value} · set at launch`}; + return requested.value === null ? {state: 'default', words: 'model default · set in NMSh'} : {value: requested.value, state: 'acknowledged', words: `${requested.value} · acknowledged by Claude`}; + } + if (settingsEffort) return {value: settingsEffort, state: 'settings', words: `${settingsEffort} · from Claude settings`}; + return option ? {state: 'default', words: 'model default (level not reported)'} : {state: 'unknown', words: 'unknown until Claude publishes its models'}; +} diff --git a/src/agents/transcript/view.ts b/src/agents/transcript/view.ts index b43c7154..c7ea290c 100644 --- a/src/agents/transcript/view.ts +++ b/src/agents/transcript/view.ts @@ -10,6 +10,8 @@ import {renderControlRows, renderControls} from '../../ui/controls.js'; import {colorLevel} from '../../presentation/capabilities.js'; import {layoutInput} from '../../input/inputLayout.js'; import {chatColumn} from '../../output/TranscriptPresenter.js'; +import {visibleRows, type PickerState} from '../input/pickers.js'; +import {currentModel, effortStatus, modelName} from '../telemetry.js'; /** How the host draws the view: the transcript presentation and composer side it already uses, and whether it shows the terminal's own caret. */ export interface AgentViewOptions { @@ -28,7 +30,7 @@ type Styled = {styled: string}; type Line = string | Styled; /** What the person is doing in the view, in words; the default (writing to the agent) needs no label. */ -const OWNER_WORDS: Partial> = {TRANSCRIPT: 'Reviewing the conversation', APPROVAL: 'Permission request', CHOICE: 'Question'}; +const OWNER_WORDS: Partial> = {TRANSCRIPT: 'Reviewing the conversation', APPROVAL: 'Permission request', CHOICE: 'Question', PICKER: 'Choosing'}; /** Hanging indent of the conversation: text starts here; turn markers sit in the gutter before it. */ const INDENT = 2; @@ -120,7 +122,13 @@ function providerHeader(session: AgentSession, meta: AgentViewMeta, width: numbe const subtle = paint(UI_COLORS.subtle); const glyph = (safe() ? descriptor?.safeGlyph : descriptor?.glyph) ?? '*'; const name = displayText(descriptor?.name ?? session.harness); - const model = session.model ? displayText(session.model) : meta.configuredModel ? `${displayText(meta.configuredModel)} · configured` : ''; + const telemetry = session.telemetry; + const running = telemetry ? currentModel(telemetry, session.model) : undefined; + // Effort only as acknowledged, set at launch or read from the account's settings: never invented. + const effort = telemetry ? effortStatus(telemetry, meta.settingsEffort) : undefined; + const effortText = effort?.value && ['acknowledged', 'launch', 'settings'].includes(effort.state) ? ` · ${displayText(effort.value)} effort` : ''; + const model = running ? `${displayText(modelName(running.value, telemetry))}${running.source === 'requested' ? ' (requested)' : ''}${effortText}` + : session.model ? displayText(session.model) : meta.configuredModel ? `${displayText(meta.configuredModel)} · configured` : ''; const identity = displayText(meta.profileLabel ?? meta.identity ?? ''); const cwd = session.cwd ? homeRelative(displayText(session.cwd)) : ''; const branch = meta.branch ? displayText(meta.branch) : ''; @@ -258,10 +266,43 @@ function composer(session: AgentSession, view: AgentViewState, width: number, ha return {rows: out, caret}; } +/** + * The /model or /effort picker in the composer's place: a title with the current value and its source, the rows + * (selection marker, current and default marks in words so nothing depends on color), the selected row's description, + * when the change applies, and the picker's own message. Bounded to `room` rows; the selection stays visible. + */ +function pickerRows(picker: PickerState, width: number, room: number): Line[] { + const g = marks(); + const primary = paint(UI_COLORS.primary), secondary = paint(UI_COLORS.secondary), subtle = paint(UI_COLORS.subtle), accent = paint(UI_COLORS.accent); + const title = picker.kind === 'model' ? 'Model' : 'Effort'; + const head: Line[] = [{styled: `${accent}${title}${RESET}${subtle} · ${truncateText(picker.status, Math.max(1, width - displayWidth(title) - 3))}${RESET}`}]; + if (picker.query) head.push({styled: `${subtle}Filter: ${RESET}${primary}${truncateText(picker.query, Math.max(1, width - 8))}${RESET}`}); + const rows = visibleRows(picker); + const tail: Line[] = []; + const chosen = rows[picker.selected]; + if (chosen?.detail) tail.push({styled: `${secondary}${truncateText(chosen.detail, width)}${RESET}`}); + tail.push({styled: `${subtle}${truncateText(picker.busy ? 'Waiting for Claude to acknowledge' + g.ellipsis : picker.timing, width)}${RESET}`}); + if (picker.message) tail.push({styled: `${paint(UI_COLORS.failure)}${truncateText(picker.message, width)}${RESET}`}); + const listRoom = Math.max(1, room - head.length - tail.length); + const start = Math.max(0, Math.min(picker.selected - listRoom + 1, rows.length - listRoom)); + const list: Line[] = rows.length ? rows.slice(start, start + listRoom).map((row, offset) => { + const index = start + offset; + const selected = index === picker.selected; + const lead = selected ? `${accent}${GLYPHS.selection}${RESET} ` : ' '; + const tags = [row.current ? 'current' : '', row.isDefault && picker.kind === 'model' ? 'default' : ''].filter(Boolean).join(', '); + const tag = tags ? ` ${subtle}(${tags})${RESET}` : ''; + const label = truncateText(row.label, Math.max(1, width - 2 - (tags ? tags.length + 4 : 0))); + return {styled: `${lead}${selected ? primary : secondary}${label}${RESET}${tag}`}; + }) : [{styled: `${subtle} Nothing matches "${truncateText(picker.query, Math.max(1, width - 20))}"${RESET}`}]; + return [...head, ...list, ...tail].slice(0, Math.max(1, room)); +} + /** What the keys do right now, with the shared footer styling (keys accent, actions muted); controls are dropped whole, never cut. */ function hints(session: AgentSession, view: AgentViewState, width: number): Line[] { const c = view.controller!; - const pairs: Array<[string, string]> = c.owner === 'TRANSCRIPT' + const pairs: Array<[string, string]> = c.owner === 'PICKER' + ? [['Enter', 'apply'], ['↑↓', 'choose'], ['Esc', 'cancel'], ['type', 'filter']] + : c.owner === 'TRANSCRIPT' ? [['↑↓', 'objects'], ['←→', 'zoom'], ['1-5', 'detail'], ['c', 'copy visible'], ['C', 'copy full'], ['Esc', 'shell']] : [['Enter', 'send'], ...(session.state === 'working' || session.state === 'approval' ? [['Ctrl+C', 'interrupt'] as [string, string]] : []), ['Tab', 'transcript'], ['Esc', 'shell']]; const keys = safe() ? pairs.map(([key, label]) => [key.replace('↑↓', 'Up/Down').replace('←→', 'Left/Right'), label] as [string, string]) : pairs; @@ -308,6 +349,7 @@ export function renderSemanticAgentView(session: AgentSession, view: AgentViewSt if (source?.incomplete) notices.push(...wrap(`Incomplete: ${source.incomplete}`, paint(UI_COLORS.failure))); const composing = c.owner === 'AGENT_MESSAGE' && session.level === 'managed' && !['exited', 'failed'].includes(session.state); const draft = composing ? composer(session, view, width, Boolean(options.hardwareCaret)) : undefined; + const picking = c.owner === 'PICKER' && c.picker ? pickerRows(c.picker, width, Math.max(3, Math.min(14, Math.floor(height / 2)))) : undefined; const controls: Line[] = session.level === 'managed' ? hints(session, view, width) : wrap('Observed metadata only · Esc shell', subtle); const rule = stateRule(session, width); const top = options.composerPosition === 'top'; @@ -325,7 +367,7 @@ export function renderSemanticAgentView(session: AgentSession, view: AgentViewSt while (body.at(-1) === '') body.pop(); stream = true; } - const draftRows = draft?.rows ?? []; + const draftRows = picking ?? draft?.rows ?? []; const room = Math.max(1, height - notices.length - 1 - draftRows.length - controls.length); if (stream) { // Scrolled back: rows that arrive below keep the visible ones where they are. @@ -342,7 +384,7 @@ export function renderSemanticAgentView(session: AgentSession, view: AgentViewSt : [...(stream ? [...filler, ...visible] : [...visible, ...filler]), ...notices, rule, ...draftRows, ...controls]; if (options.layout) { const at = top ? 0 : rows.length - controls.length - draftRows.length; - options.layout.caret = draft && at + draft.caret.row < height ? {row: at + draft.caret.row, column: Math.min(width - 1, draft.caret.column)} : undefined; + options.layout.caret = draft && !picking && at + draft.caret.row < height ? {row: at + draft.caret.row, column: Math.min(width - 1, draft.caret.column)} : undefined; } return rows.slice(0, height).map(emit); } diff --git a/src/app/TerminalApp.ts b/src/app/TerminalApp.ts index 2218896d..ba5d54bb 100644 --- a/src/app/TerminalApp.ts +++ b/src/app/TerminalApp.ts @@ -310,6 +310,9 @@ import {safeGithubUrl} from '../githubWorkspace/sanitize.js'; import {GithubWorkspaceService} from '../githubWorkspace/service.js'; import {GhSource} from '../githubWorkspace/source.js'; import {loadModInventory} from '../agents/mods/inventory.js'; +import {openEffortPicker, openModelPicker} from '../agents/input/pickers.js'; +import {applyPickerChoice} from '../agents/input/surface.js'; +import {settingsEffort} from '../agents/claudeSettings.js'; import {agentBlocks, agentPanelRows, renderAgentPanel, renderAgentView, renderShelf, shelfOrder, type AgentPanelState, type AgentViewMeta, type AgentViewState} from '../agents/sessions/AgentViews.js'; import {harness} from '../agents/harnesses.js'; import {defaultRuntimeDir, socketPathFor} from '../session/runtimeDir.js'; @@ -8018,6 +8021,12 @@ export class TerminalApp { answer: (id, requestId, allow) => this.agents.get(id)?.pendingApproval?.requestId === requestId && this.agents.answer(id, allow), cancel: id => {this.agents.cancel(id);}, choose: (id, requestId, answers) => this.agents.choose(id, requestId, answers), + openPicker: (target, kind) => { + if (!this.agents.controllable(target.id)) return {ok: false, reason: `/${kind} needs a running managed Claude target with a live control channel.`}; + return kind === 'model' ? openModelPicker(target.telemetry) : openEffortPicker(target.telemetry, this.agentSettingsEffort.get(target.id)); + }, + applyPicker: (target, picker, row) => {void applyPickerChoice(view, picker, row, { + setModel: model => this.agents.setModel(target.id, model), setEffort: level => this.agents.setEffort(target.id, level), render: () => { if (!this.stopped) this.render(); }});}, copy: text => {void writeClipboard(text).then(() => {view.message = 'Copied.'; this.render();}).catch(() => {view.message = 'Clipboard unavailable.'; this.render();});}, copyReply: (target, index) => {void this.copyAgentBlock(target, index);}, }); @@ -8112,12 +8121,20 @@ export class TerminalApp { this.render(); } + /** Effort from each target's Claude settings files, read once off the render path. */ + private readonly agentSettingsEffort = new Map(); + /** Header facts for an agent view: the launch profile's label and configured model, and the shell's branch when the target shares its directory. */ private agentViewMeta(session: AgentSession): AgentViewMeta { const profile = session.profileId ? this.promptConfiguration.agentProfiles.find(item => item.name === session.profileId) : undefined; + if (session.harness === 'claude' && session.cwd && !this.agentSettingsEffort.has(session.id)) { + this.agentSettingsEffort.set(session.id, undefined); + void settingsEffort(profile?.configDir, session.cwd).then(level => { if (level && !this.stopped) { this.agentSettingsEffort.set(session.id, level); this.render(); } }); + } + const effort = this.agentSettingsEffort.get(session.id); const defaultDir = process.env.CLAUDE_CONFIG_DIR ? process.env.CLAUDE_CONFIG_DIR.replace(/^\/(?:Users|home)\/[^/]+/u, '~') : '~/.claude'; return {...(profile ? {profileLabel: profileLabel(profile), ...(profile.model ? {configuredModel: profile.model} : {})} : {identity: session.harness === 'claude' ? `Default identity · ${defaultDir}` : 'Default identity'}), - ...(session.cwd === this.shellCwd && this.context.branch ? {branch: this.context.branch} : {})}; + ...(session.cwd === this.shellCwd && this.context.branch ? {branch: this.context.branch} : {}), ...(effort ? {settingsEffort: effort} : {})}; } private openMods(provider?: string): void { diff --git a/tests/agentPickers.test.ts b/tests/agentPickers.test.ts new file mode 100644 index 00000000..fa8ff0a2 --- /dev/null +++ b/tests/agentPickers.test.ts @@ -0,0 +1,200 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {pushEvent, type AgentSession} from '../src/agents/sessions/model.js'; +import {AgentTranscript} from '../src/agents/transcript/model.js'; +import {renderSemanticAgentView} from '../src/agents/transcript/view.js'; +import {AgentInputController} from '../src/agents/input/controller.js'; +import {applyPickerChoice, handleAgentInput, pickerCommand, type AgentSurfaceHost} from '../src/agents/input/surface.js'; +import {openEffortPicker, openModelPicker, pickerArgument, visibleRows, type PickerRow, type PickerState} from '../src/agents/input/pickers.js'; +import {parseCatalog} from '../src/agents/sessions/claudeStream.js'; +import {applyTelemetry, effortStatus, emptyTelemetry, type AgentTelemetry} from '../src/agents/telemetry.js'; +import type {AgentViewState} from '../src/agents/sessions/AgentViews.js'; +import {setIconStyle} from '../src/ui/glyphs.js'; +import {displayWidth, stripAnsi} from '../src/util/text.js'; +import type {Key} from '../src/terminal/keys.js'; + +/* The model catalog exactly as Claude Code 2.1.295 published it in the recorded control probe. */ +const CATALOG = parseCatalog({commands: [], agents: [], models: [ + {value: 'default', resolvedModel: 'claude-opus-5-5', displayName: 'Default (recommended)', description: 'Opus 5.5 · Most capable', supportsEffort: true, supportedEffortLevels: ['low', 'medium', 'high', 'xhigh', 'max']}, + {value: 'sonnet', resolvedModel: 'claude-sonnet-5-5', displayName: 'Sonnet 5.5', description: 'Fast and capable', supportsEffort: true, supportedEffortLevels: ['low', 'medium', 'high', 'xhigh', 'max']}, + {value: 'claude-haiku-4-5-20251001', resolvedModel: 'claude-haiku-4-5-20251001', displayName: 'Haiku 4.5', description: 'Fastest; no effort levels'}, +]}, 1)!; + +function telemetry(model = 'claude-opus-5-5'): AgentTelemetry { + const t = emptyTelemetry(); + applyTelemetry(t, {kind: 'catalog', catalog: CATALOG}); + applyTelemetry(t, {kind: 'runtime', runtime: {model, tools: [], mcpServers: [], slashCommands: [], skills: [], plugins: [], capabilities: [], at: 2}}); + return t; +} +function session(extra: Partial = {}): AgentSession { + const s: AgentSession = {id: 'a', harness: 'claude', level: 'managed', title: 'Picker', cwd: '/w', startedAt: 0, state: 'waiting', events: [], attention: false, updatedAt: 0, + transcript: new AgentTranscript(), telemetry: telemetry(), ...extra}; + pushEvent(s, {kind: 'user', text: 'hello'}); + return s; +} +function view(input = ''): AgentViewState { return {sessionId: 'a', input, expanded: new Set(), scroll: 0, controller: new AgentInputController()}; } +const key = (kind: string, value = ''): Key => ({kind, value} as unknown as Key); +const type = (v: AgentViewState, s: AgentSession, host: AgentSurfaceHost, text: string) => { for (const ch of text) handleAgentInput(v, s, key('text', ch), host); }; + +test('/model and /effort are NMSh commands only in their exact forms; a leading space sends text to Claude verbatim', () => { + assert.deepEqual(pickerCommand('/model'), {kind: 'model', argument: ''}); + assert.deepEqual(pickerCommand('/model sonnet'), {kind: 'model', argument: 'sonnet'}); + assert.deepEqual(pickerCommand('/effort high '), {kind: 'effort', argument: 'high'}); + for (const text of [' /model', '/models', '/model a b', '/effortless', 'model', '/model\nsonnet']) assert.equal(pickerCommand(text), undefined, JSON.stringify(text)); +}); + +test('model picker: rows are the provider catalog; the running model is current and reported, a request is pending', () => { + const t = telemetry('claude-sonnet-5-5'); + const opened = openModelPicker(t); + assert.ok(opened.ok); + assert.deepEqual(opened.picker.rows.map(row => [row.value, row.label, Boolean(row.current), Boolean(row.isDefault)]), + [['default', 'Default (recommended)', false, true], ['sonnet', 'Sonnet 5.5', true, false], ['claude-haiku-4-5-20251001', 'Haiku 4.5', false, false]]); + assert.equal(opened.picker.selected, 1, 'the cursor starts on the current model'); + assert.equal(opened.picker.status, 'Running Sonnet 5.5 · reported by Claude'); + applyTelemetry(t, {kind: 'requested', field: 'model', value: 'claude-haiku-4-5-20251001', at: 5}); + const pending = openModelPicker(t); + assert.ok(pending.ok); + assert.match(pending.picker.status, /Haiku 4\.5 requested · Claude confirms it/u); + assert.equal(openModelPicker(emptyTelemetry()).ok, false, 'no catalog: the picker says why instead of guessing'); + assert.equal(pickerArgument(opened.picker, 'Sonnet 5.5')?.value, 'sonnet'); + assert.equal(pickerArgument(opened.picker, 'son'), undefined, 'an argument applies only on an exact match'); +}); + +test('effort picker: only the running model\'s levels, with provenance; a model without levels has none to pick', () => { + const t = telemetry('claude-opus-5-5'); + const opened = openEffortPicker(t, 'low'); + assert.ok(opened.ok); + assert.deepEqual(opened.picker.rows.map(row => row.value), [null, 'low', 'medium', 'high', 'xhigh', 'max']); + assert.equal(opened.picker.rows.find(row => row.current)?.value, 'low'); + assert.equal(opened.picker.status, 'Effort: low · from Claude settings'); + assert.match(opened.picker.timing, /next turn, for this session only; Claude's settings files are not changed/u); + assert.deepEqual(effortStatus(t), {state: 'default', words: 'model default (level not reported)'}, 'nothing configured: unreported, never invented'); + applyTelemetry(t, {kind: 'requested', field: 'effort', value: 'high', at: 9}); + assert.equal(effortStatus(t, 'low').words, 'high · acknowledged by Claude', 'an acknowledged request outranks settings'); + applyTelemetry(t, {kind: 'requested', field: 'effort', value: 'max', at: 10, evidence: 'launch flag'}); + assert.equal(effortStatus(t).state, 'launch'); + const haiku = openEffortPicker(telemetry('claude-haiku-4-5-20251001')); + assert.deepEqual(haiku, {ok: false, reason: 'Haiku 4.5 has no effort levels to choose from.'}); + assert.equal(effortStatus(telemetry('claude-haiku-4-5-20251001'), 'high').state, 'unavailable'); + const unknownModel = emptyTelemetry(); + applyTelemetry(unknownModel, {kind: 'catalog', catalog: CATALOG}); + assert.equal(openEffortPicker(unknownModel).ok, false, 'the running model is unknown: its levels are too'); +}); + +test('keyboard: /model opens the picker; it owns the keys, filters as you type, Esc backs out in steps, Enter applies the selection', () => { + const s = session(); + const v = view(); + const applied: PickerRow[] = []; + const sent: string[] = []; + const host: AgentSurfaceHost = {shellEmpty: true, send: (_id, text) => { sent.push(text); return true; }, answer: () => false, cancel: () => {}, copy: () => {}, copyReply: () => {}, + openPicker: (target, kind) => kind === 'model' ? openModelPicker(target.telemetry) : openEffortPicker(target.telemetry), + applyPicker: (_target, _picker, row) => { applied.push(row); }}; + type(v, s, host, '/model'); + handleAgentInput(v, s, key('enter'), host); + const c = v.controller!; + assert.equal(c.owner, 'PICKER'); + assert.equal(v.input, '', 'the command left the draft'); + assert.deepEqual(sent, [], 'nothing was sent to Claude'); + type(v, s, host, 'hai'); + assert.deepEqual(visibleRows(c.picker!).map(row => row.label), ['Haiku 4.5']); + handleAgentInput(v, s, key('escape'), host); + assert.equal(c.owner, 'PICKER', 'the first Esc clears the filter'); + assert.equal(c.picker!.query, ''); + handleAgentInput(v, s, key('down'), host); + handleAgentInput(v, s, key('down'), host); + handleAgentInput(v, s, key('down'), host); + handleAgentInput(v, s, key('enter'), host); + assert.deepEqual(applied.map(row => row.value), ['claude-haiku-4-5-20251001'], 'the selection stops at the last row'); + handleAgentInput(v, s, key('escape'), host); + assert.equal(c.owner, 'AGENT_MESSAGE'); + assert.equal(c.picker, undefined); + // Ordinary text and a verbatim leading-space command still go to Claude. + type(v, s, host, ' /model'); + handleAgentInput(v, s, key('enter'), host); + assert.deepEqual(sent, [' /model']); + // Observed targets never open a picker; a host without the control channel explains. + const unsupported: AgentSurfaceHost = {...host, openPicker: () => ({ok: false, reason: '/effort needs a running managed Claude target with a live control channel.'})}; + type(v, s, unsupported, '/effort'); + handleAgentInput(v, s, key('enter'), unsupported); + assert.equal(c.owner, 'AGENT_MESSAGE'); + assert.match(v.message ?? '', /live control channel/u); +}); + +test('/model sonnet applies directly on an exact match; an unknown name opens the picker with a note', () => { + const s = session(); + const v = view(); + const applied: Array = []; + const host: AgentSurfaceHost = {shellEmpty: true, send: () => true, answer: () => false, cancel: () => {}, copy: () => {}, copyReply: () => {}, + openPicker: target => openModelPicker(target.telemetry), applyPicker: (_t, _p, row) => { applied.push(row.value); }}; + type(v, s, host, '/model sonnet'); + handleAgentInput(v, s, key('enter'), host); + assert.deepEqual(applied, ['sonnet']); + assert.notEqual(v.controller!.owner, 'PICKER'); + type(v, s, host, '/model gpt-9'); + handleAgentInput(v, s, key('enter'), host); + assert.equal(v.controller!.owner, 'PICKER'); + assert.match(v.controller!.picker!.message ?? '', /No model named "gpt-9"/u); +}); + +test('apply: success closes the picker with the provider\'s acknowledgement and timing; refusal keeps it open with the reason', async () => { + const v = view(); + const c = v.controller!; + const opened = openModelPicker(telemetry()); + assert.ok(opened.ok); + c.picker = opened.picker; c.owner = 'PICKER'; + const row = opened.picker.rows[1]!; + const refused = await applyPickerChoice(v, opened.picker, row, {setModel: async () => ({ok: false, reason: 'The provider did not answer in time'}), setEffort: async () => ({ok: true, value: undefined}), render: () => {}}); + assert.equal(refused, false); + assert.equal(c.owner, 'PICKER'); + assert.equal(opened.picker.message, 'Not changed: The provider did not answer in time'); + assert.equal(opened.picker.busy, false); + const ok = await applyPickerChoice(v, opened.picker, row, {setModel: async model => ({ok: model === 'sonnet', value: undefined} as {ok: true; value: undefined}), setEffort: async () => ({ok: true, value: undefined}), render: () => {}}); + assert.equal(ok, true); + assert.equal(c.owner, 'AGENT_MESSAGE'); + assert.match(v.message ?? '', /^Model → Sonnet 5\.5\. Claude acknowledged; it applies from the next model call and is confirmed when Claude reports it\.$/u); +}); + +test('rendering: the picker takes the composer\'s place at every width, in Safe glyphs and NO_COLOR, with words for every mark', () => { + const s = session(); + const v = view(); + const opened = openEffortPicker(s.telemetry, 'medium'); + assert.ok(opened.ok); + v.controller!.picker = opened.picker; v.controller!.owner = 'PICKER'; + for (const columns of [30, 50, 80, 140]) { + for (const height of [10, 24]) { + const rows = renderSemanticAgentView(s, v, columns, height); + const text = rows.map(row => stripAnsi(row)); + assert.equal(rows.length, height, `${columns}x${height}`); + assert.ok(text.every(row => displayWidth(row) <= columns), `${columns}x${height} fits`); + assert.ok(text.some(row => row.startsWith('Effort ·')), `${columns}x${height} title`); + assert.ok(text.some(row => /medium {2}\(current\)/u.test(row)), `${columns}x${height} current in words`); + assert.match(text.at(-1)!, /Enter/u, 'controls stay visible'); + } + } + const before = process.env.NO_COLOR; + setIconStyle('safe'); + process.env.NO_COLOR = '1'; + try { + const rows = renderSemanticAgentView(s, v, 60, 16); + const joined = rows.join('\n'); + assert.doesNotMatch(joined, /\u001b\[(?:38|48);/u, 'no color under NO_COLOR'); + assert.match(stripAnsi(joined), /^> medium {2}\(current\)$/mu, 'selection reads as ">" in Safe glyphs'); + assert.match(stripAnsi(joined), /Up\/Down choose/u); + assert.doesNotMatch(stripAnsi(joined), /[›▏◆↑↓]/u); + } finally { setIconStyle('nerd'); if (before === undefined) delete process.env.NO_COLOR; else process.env.NO_COLOR = before; } +}); + +test('header: model and effort with provenance; requested changes read as requested; effort never invented', () => { + const s = session(); + const top = (rows: string[]) => stripAnsi(rows.find(row => stripAnsi(row).includes('Picker')) ?? ''); + const header = () => top(renderSemanticAgentView(s, view(), 100, 12)); + assert.match(header(), /Opus 5\.5$/u, 'no effort known: none shown'); + assert.match(top(renderSemanticAgentView(s, view(), 100, 12, {settingsEffort: 'low'})), /Opus 5\.5 · low effort$/u); + applyTelemetry(s.telemetry!, {kind: 'requested', field: 'model', value: 'sonnet', at: 10}); + applyTelemetry(s.telemetry!, {kind: 'requested', field: 'effort', value: 'high', at: 10}); + assert.match(header(), /Sonnet 5\.5 \(requested\) · high effort$/u); + applyTelemetry(s.telemetry!, {kind: 'runtime', runtime: {model: 'claude-sonnet-5-5', tools: [], mcpServers: [], slashCommands: [], skills: [], plugins: [], capabilities: [], at: 20}}); + assert.match(header(), /Sonnet 5\.5 · high effort$/u, 'once Claude reports the model it is no longer "requested"'); +}); + +void ({} as PickerState); From cb1551ff8d8df9a450a34c08d19b872b6443b0bf Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:42:29 +0530 Subject: [PATCH 007/249] Claude settings reader: only a bounded regular file is read A FIFO or very large file at a project's .claude/settings.json could block or flood the effort lookup. The path is checked with lstat first. --- src/agents/claudeSettings.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/agents/claudeSettings.ts b/src/agents/claudeSettings.ts index 82b69342..365c75df 100644 --- a/src/agents/claudeSettings.ts +++ b/src/agents/claudeSettings.ts @@ -1,4 +1,4 @@ -import {readFile} from 'node:fs/promises'; +import {lstat, readFile} from 'node:fs/promises'; import {homedir} from 'node:os'; import {join} from 'node:path'; import {EFFORT_LEVELS} from './sessions/claudeAdapter.js'; @@ -14,8 +14,10 @@ export async function settingsEffort(configDir: string | undefined, cwd: string) let found: string | undefined; for (const path of [user, join(cwd, '.claude', 'settings.json'), join(cwd, '.claude', 'settings.local.json')]) { try { + // Only a regular file of bounded size is read: a FIFO or device at a project path must never block or flood NMSh. + const info = await lstat(path); + if (!info.isFile() || info.size > 256 * 1024) continue; const text = await readFile(path, 'utf8'); - if (text.length > 1024 * 1024) continue; const value = (JSON.parse(text) as {effortLevel?: unknown}).effortLevel; if (typeof value === 'string' && (EFFORT_LEVELS as readonly string[]).includes(value)) found = value; } catch { /* absent or unreadable: no fact */ } From a58090822a1c5fd786396ac1d0fced5703b0b60d Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:46:55 +0530 Subject: [PATCH 008/249] Claude mods: real types and states, toggles through Claude's own command, live reload /claude mods and /mods now show each account's Claude plugins as they are: - Type from the plugin's own manifest files, read as data (nothing is imported or run): Mod (hook modules), Plugin, Settings hook or Skill, with its components. - The effective state for this directory. The old inventory read projectEnabled, which is false for nearly every installed plugin, so everything showed disabled; project and local enabledPlugins overrides are now read and named instead. - Conflicts (also installed at another scope, other version), dependencies and options, in words; project installations of other directories are not shown. - What it does in an NMSh-managed session, per Claude's documented behavior for claude -p: a mod's hooks run, nothing it draws appears; skills, agents, commands, hooks, MCP and LSP load. "Loaded in" names targets whose own report lists it. Space proposes a change and Enter confirms it: claude plugin enable|disable at the plugin's installed scope and account (CLAUDE_CONFIG_DIR), --json. Only ids from the current listing are sent, since Claude's command writes an unknown id into settings. The listing is read again as evidence, then running managed sessions on that account reload plugins (reload_plugins) and the result is reported, errors included. Standalone skills and settings hooks are listed without a switch, with the reason. i inspects through claude plugin details and validate (static). No sandbox is implied. --- src/agents/mods/claudeDiscovery.ts | 17 +-- src/agents/mods/claudePlugins.ts | 220 +++++++++++++++++++++++++++++ src/agents/mods/controller.ts | 35 ++++- src/agents/mods/inventory.ts | 27 +++- src/agents/mods/model.ts | 15 ++ src/agents/mods/view.ts | 55 ++++++-- src/agents/sessions/manager.ts | 15 ++ src/app/TerminalApp.ts | 56 +++++++- tests/claudePlugins.test.ts | 207 +++++++++++++++++++++++++++ tests/modsMenu.test.ts | 12 +- 10 files changed, 627 insertions(+), 32 deletions(-) create mode 100644 src/agents/mods/claudePlugins.ts create mode 100644 tests/claudePlugins.test.ts diff --git a/src/agents/mods/claudeDiscovery.ts b/src/agents/mods/claudeDiscovery.ts index a5da2e16..98485351 100644 --- a/src/agents/mods/claudeDiscovery.ts +++ b/src/agents/mods/claudeDiscovery.ts @@ -1,12 +1,9 @@ -import {execFile} from 'node:child_process'; -import {promisify} from 'node:util'; import {createHash} from 'node:crypto'; import {displayText} from '../transcript/projection.js'; import type {ModEntry} from './model.js'; -const run = promisify(execFile); const text = (value: unknown, limit = 256) => typeof value === 'string' ? displayText(value).slice(0, limit) : ''; -/** Installed 2.1.292 listing schema. No importing or reading executable plugin files. */ +/** Rows of `claude plugin list --json` alone, synchronously (no manifest reads). The live inventory uses claudePlugins.ts. */ export function claudeInventory(value: unknown, cwd: string): ModEntry[] { if (!Array.isArray(value)) throw new Error('Claude plugin listing has an unsupported shape'); return value.slice(0, 256).flatMap(raw => { @@ -15,16 +12,12 @@ export function claudeInventory(value: unknown, cwd: string): ModEntry[] { const id = text(raw.id); const reference = text(raw.installPath, 512); const scope = raw.scope === 'user' ? 'global' : raw.scope === 'project' || raw.scope === 'local' ? 'project' : 'unknown'; const key = createHash('sha256').update(JSON.stringify([raw.id, raw.scope, raw.projectPath, raw.installPath])).digest('hex'); - const enabled = typeof raw.projectEnabled === 'boolean' ? raw.projectEnabled : raw.enabled; + // `enabled` is the installation's state; `projectEnabled` only says whether project settings name it (false for + // nearly every user-scope plugin), so it never decides the state shown. Project overrides: claudePlugins.ts. + const enabled = raw.enabled; return [{key: `claude:${key}`, id, name: id, kind: 'Provider-native' as const, provider: 'claude', source: 'Claude Code plugin list --json', ...(text(raw.version) ? {version: text(raw.version)} : {}), scope, ...(reference ? {reference} : {}), installedBy: 'unknown' as const, enabled: enabled === true ? 'yes' as const : enabled === false ? 'no' as const : 'unknown' as const, managed: false, executesInside: 'Claude Code', sandbox: 'No' as const, - permissions: [], applicability: ['claude'], evidence: typeof raw.projectEnabled === 'boolean' ? 'Provider projectEnabled for current workspace; installation enabled state may differ' : 'Provider listing; provenance and privileges unknown'}]; + permissions: [], applicability: ['claude'], evidence: 'Provider listing; provenance and privileges unknown'}]; }); } - -/** Trusted provider's supported passive command; bounded, no marketplace sync or activation. */ -export async function discoverClaudePlugins(executable: string, cwd: string, configDir?: string): Promise { - const {stdout} = await run(executable, ['plugin', 'list', '--json'], {cwd, ...(configDir ? {env: {...process.env, CLAUDE_CONFIG_DIR: configDir}} : {}), timeout: 5000, maxBuffer: 1024 * 1024, encoding: 'utf8'}); - return claudeInventory(JSON.parse(stdout), cwd); -} diff --git a/src/agents/mods/claudePlugins.ts b/src/agents/mods/claudePlugins.ts new file mode 100644 index 00000000..c5f6d3d0 --- /dev/null +++ b/src/agents/mods/claudePlugins.ts @@ -0,0 +1,220 @@ +import {execFile} from 'node:child_process'; +import {promisify} from 'node:util'; +import {lstat, readdir, readFile} from 'node:fs/promises'; +import {join} from 'node:path'; +import {createHash} from 'node:crypto'; +import {displayText} from '../transcript/projection.js'; +import type {ModComponents, ModEntry} from './model.js'; + +/** + * Claude Code plugins and mods through Claude's own supported surface: `claude plugin list --json` for what is + * installed and enabled, static reads of each plugin's manifest files for what it contains, and `claude plugin + * enable|disable --scope … --json` for changes. Plugin code is never imported or run by discovery: manifests are + * read as bounded JSON, and component folders are only counted. + */ + +const run = promisify(execFile); +const text = (value: unknown, limit = 256) => typeof value === 'string' ? displayText(value).replace(/\n/gu, ' ').slice(0, limit) : ''; +const MAX_JSON = 256 * 1024; +const PLUGIN_ID = /^[\w.-]{1,96}@[\w.-]{1,96}$/u; + +/** One row of `claude plugin list --json` (Claude Code 2.1.29x), as far as NMSh reads it. */ +export interface ClaudeListing { + id: string; + version?: string; + scope: 'user' | 'project' | 'local' | string; + enabled?: boolean; + installPath?: string; + projectPath?: string; + hasUserConfig?: boolean; + readFromFolder?: string; +} + +async function readJson(path: string): Promise | undefined> { + try { + const info = await lstat(path); + if (!info.isFile() || info.size > MAX_JSON) return undefined; + const value = JSON.parse(await readFile(path, 'utf8')) as unknown; + return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : undefined; + } catch { return undefined; } +} + +async function count(path: string, match: (name: string, isDir: boolean) => boolean): Promise { + try { return (await readdir(path, {withFileTypes: true})).slice(0, 512).filter(entry => match(entry.name, entry.isDirectory())).length; } catch { return 0; } +} + +/** What a plugin contains, from its manifest files only (the documented layout); nothing is executed. */ +export async function readComponents(installPath: string): Promise<{components: ModComponents; name?: string; description?: string; dependencies: string[]}> { + const manifest = await readJson(join(installPath, '.claude-plugin', 'plugin.json')) ?? {}; + const hooksFile = await readJson(join(installPath, 'hooks', 'hooks.json')) ?? {}; + // A mod declares hook modules; a settings-style hook file maps events to commands. + const modules = Array.isArray(hooksFile.modules) ? hooksFile.modules.length : 0; + const events = hooksFile.hooks && typeof hooksFile.hooks === 'object' ? Object.keys(hooksFile.hooks as object).length : 0; + const mcp = Boolean(await readJson(join(installPath, '.mcp.json'))) || Boolean(manifest.mcpServers); + const lsp = Boolean(await readJson(join(installPath, '.lsp.json'))) || Boolean(manifest.lspServers); + const dependencies = Array.isArray(manifest.dependencies) ? manifest.dependencies.flatMap(item => typeof item === 'string' ? [text(item, 120)] : item && typeof item === 'object' && typeof (item as {name?: unknown}).name === 'string' ? [text((item as {name: string}).name, 120)] : []).slice(0, 16) : []; + return { + components: {mod: modules > 0, hookEvents: events, skills: await count(join(installPath, 'skills'), (_name, isDir) => isDir), agents: await count(join(installPath, 'agents'), name => name.endsWith('.md')), + commands: await count(join(installPath, 'commands'), name => name.endsWith('.md')), mcp, lsp}, + ...(text(manifest.name) ? {name: text(manifest.name, 96)} : {}), + ...(text(manifest.description) ? {description: text(manifest.description, 600)} : {}), + dependencies, + }; +} + +/** The plugin's type in words: a mod is code inside Claude Code; anything else is a plugin of declarative parts and scripts. */ +export function nativeType(components: ModComponents | undefined): 'Mod' | 'Plugin' { + return components?.mod ? 'Mod' : 'Plugin'; +} + +/** + * What a plugin does in an NMSh-managed Claude session (`claude --print` with stream-json), per Claude's documented + * behavior: a mod's hooks run, but nothing it draws appears; skills, agents, commands, settings hooks, MCP and LSP + * servers load as in any session. + */ +export function managedRuntime(components: ModComponents | undefined, drawsOnly?: boolean): string { + if (!components) return 'Loads in managed sessions like any Claude session'; + const parts = [components.skills ? `${components.skills} skill${components.skills === 1 ? '' : 's'}` : '', components.agents ? `${components.agents} agent${components.agents === 1 ? '' : 's'}` : '', + components.commands ? `${components.commands} command${components.commands === 1 ? '' : 's'}` : '', components.hookEvents ? 'settings hooks' : '', components.mcp ? 'MCP server' : '', components.lsp ? 'LSP server' : ''].filter(Boolean); + if (components.mod) { + const mod = drawsOnly ? 'Mod draws only in Claude\'s own interface: nothing of it appears in NMSh-managed sessions' : 'Mod hooks run in NMSh-managed sessions; panes, bands and restyled rows it draws do not appear there'; + return parts.length ? `${mod}; ${parts.join(', ')} load` : mod; + } + return parts.length ? `${parts.join(', ')} load in NMSh-managed sessions` : 'Loads in managed sessions'; +} + +/** Project-level enabledPlugins overrides for this directory (local wins over project), read passively. */ +export async function projectOverrides(cwd: string): Promise> { + const overrides = new Map(); + for (const file of ['settings.json', 'settings.local.json']) { + const value = await readJson(join(cwd, '.claude', file)); + const plugins = value?.enabledPlugins; + if (!plugins || typeof plugins !== 'object') continue; + for (const [id, enabled] of Object.entries(plugins as Record).slice(0, 256)) if (typeof enabled === 'boolean' && PLUGIN_ID.test(id)) overrides.set(id, {enabled, file: `.claude/${file}`}); + } + return overrides; +} + +/** Inventory rows from Claude's own listing, with each plugin's components and the effective state for `cwd`. */ +export async function claudePluginEntries(listing: unknown, cwd: string, profile?: {name: string; configDir?: string}): Promise { + if (!Array.isArray(listing)) throw new Error('Claude plugin listing has an unsupported shape'); + const overrides = await projectOverrides(cwd); + const rows = listing.slice(0, 256).filter((raw): raw is ClaudeListing => Boolean(raw) && typeof raw === 'object' && typeof (raw as ClaudeListing).id === 'string' && PLUGIN_ID.test((raw as ClaudeListing).id)); + // Project and local installations belong to one directory: only this one's are shown. + const here = rows.filter(raw => raw.scope === 'user' || ((raw.scope === 'project' || raw.scope === 'local') && raw.projectPath === cwd)); + const scopesById = new Map(); + for (const raw of here) scopesById.set(raw.id, [...(scopesById.get(raw.id) ?? []), raw]); + const entries: ModEntry[] = []; + for (const raw of here) { + const installPath = typeof raw.installPath === 'string' ? raw.installPath : undefined; + const read = installPath ? await readComponents(installPath) : undefined; + const [name, marketplace] = raw.id.split('@') as [string, string]; + const scope = raw.scope === 'user' ? 'global' as const : 'project' as const; + const override = raw.scope === 'user' ? overrides.get(raw.id) : undefined; + const enabled = override ? override.enabled : raw.enabled; + const others = (scopesById.get(raw.id) ?? []).filter(item => item !== raw); + const notes = [ + ...(override && override.enabled !== raw.enabled ? [`${override.enabled ? 'Enabled' : 'Disabled'} for this project by ${override.file} (user setting: ${raw.enabled ? 'enabled' : 'disabled'})`] : []), + ...others.map(item => `Also installed at ${item.scope} scope${item.version && item.version !== raw.version ? ` (version ${text(item.version, 24)})` : ''}`), + ...(read?.dependencies.length ? [`Depends on ${read.dependencies.join(', ')}`] : []), + ...(raw.hasUserConfig ? ['Has options; configure them with claude plugin configure'] : []), + ]; + const key = createHash('sha256').update(JSON.stringify([profile?.name, raw.id, raw.scope, raw.projectPath])).digest('hex').slice(0, 24); + entries.push({key: `claude:${key}`, id: raw.id, name: read?.name ?? text(name, 96), kind: 'Provider-native', provider: 'claude', + ...(profile ? {profileId: profile.name} : {}), + ...(read?.description ? {description: read.description} : {}), + source: `${text(marketplace, 96)} marketplace${raw.readFromFolder ? ' (local folder)' : ''}`, ...(raw.version ? {version: text(raw.version, 24)} : {}), + scope, ...(installPath ? {reference: text(installPath, 512)} : {}), installedBy: 'provider', + enabled: enabled === true ? 'yes' : enabled === false ? 'no' : 'unknown', managed: false, executesInside: 'Claude Code', sandbox: 'No', + permissions: [], applicability: ['claude'], evidence: override ? `claude plugin list --json; overridden by ${override.file}` : 'claude plugin list --json', + nativeType: nativeType(read?.components), ...(read ? {components: read.components} : {}), runtime: managedRuntime(read?.components), notes, + claude: {pluginId: raw.id, scope: raw.scope === 'user' || raw.scope === 'project' || raw.scope === 'local' ? raw.scope : 'user', ...(raw.projectPath ? {projectPath: raw.projectPath} : {}), ...(profile?.configDir ? {configDir: profile.configDir} : {})}, + toggle: override ? {supported: false, reason: `This project's ${override.file} decides it here; NMSh changes only the scope a plugin is installed at`} : {supported: true}}); + } + return entries; +} + +/** Standalone skills in a launch identity's skills folder: listed truthfully; Claude has no command to toggle them. */ +export async function standaloneSkills(configDir: string, profile?: string): Promise { + const root = join(configDir, 'skills'); + let names: string[] = []; + try { names = (await readdir(root, {withFileTypes: true})).filter(entry => entry.isDirectory() && entry.name !== 'synced' && !entry.name.startsWith('.')).map(entry => entry.name).slice(0, 128); } catch { return []; } + const entries: ModEntry[] = []; + for (const name of names) { + let description = ''; + try { + const head = (await readFile(join(root, name, 'SKILL.md'), 'utf8')).slice(0, 4096); + description = text(/^description:\s*(.+)$/mu.exec(head)?.[1]?.replace(/^["']|["']$/gu, ''), 400); + } catch { continue; } + entries.push({key: `claude-skill:${profile ?? ''}:${name}`, id: text(name, 96), name: text(name, 96), kind: 'Provider-native', provider: 'claude', ...(profile ? {profileId: profile} : {}), + ...(description ? {description} : {}), source: 'Personal skills folder', scope: 'global', reference: text(join(root, name), 512), installedBy: 'external', enabled: 'yes', managed: false, + executesInside: 'Claude Code', sandbox: 'No', permissions: [], applicability: ['claude'], evidence: 'SKILL.md in the skills folder', nativeType: 'Skill', + runtime: 'Claude reads it on demand in every session, managed ones included', notes: [], + toggle: {supported: false, reason: 'Claude has no command to turn a personal skill off; move or delete its folder to remove it'}}); + } + return entries; +} + +export interface ToggleResult {ok: boolean; message: string; alreadyInGoalState?: boolean} + +/** + * Enable or disable an installed plugin at the scope it is installed at, through `claude plugin enable|disable + * --scope … --json`. Only ids from the current listing are accepted: Claude's own command writes an unknown id into + * settings as if it existed, so NMSh never sends one. + */ +export async function setPluginEnabled(executable: string, entry: ModEntry, enable: boolean, installed: ReadonlySet): Promise { + const claude = entry.claude; + if (!claude || !PLUGIN_ID.test(claude.pluginId) || !installed.has(claude.pluginId)) return {ok: false, message: 'Not an installed Claude plugin in the current listing'}; + const args = ['plugin', enable ? 'enable' : 'disable', claude.pluginId, '--scope', claude.scope, '--json']; + const env = claude.configDir ? {...process.env, CLAUDE_CONFIG_DIR: claude.configDir} : process.env; + let stdout = ''; + try { + ({stdout} = await run(executable, args, {cwd: claude.projectPath ?? process.cwd(), env, timeout: 30_000, maxBuffer: 256 * 1024, encoding: 'utf8'})); + } catch (error) { + // A failed command still prints its one JSON result line (exit 1). + stdout = (error as {stdout?: string}).stdout ?? ''; + if (!stdout.trim()) return {ok: false, message: text((error as Error).message, 200) || 'Claude could not change the plugin'}; + } + const line = stdout.split('\n').map(item => item.trim()).filter(Boolean).find(item => item.startsWith('{')); + let result: {outcome?: unknown; message?: unknown; alreadyInGoalState?: unknown} | undefined; + try { result = line ? JSON.parse(line) : undefined; } catch { result = undefined; } + if (!result) return {ok: false, message: 'Claude answered in a shape NMSh does not know'}; + const alreadyInGoalState = result.alreadyInGoalState === true; + return {ok: result.outcome === 'ok' || alreadyInGoalState, message: text(result.message, 300) || (result.outcome === 'ok' ? 'Done' : 'Claude refused the change'), ...(alreadyInGoalState ? {alreadyInGoalState} : {})}; +} + +/** Static inspection through Claude's own commands: components with token cost, and a mod's hooks and calls. */ +export async function inspectPlugin(executable: string, entry: ModEntry): Promise<{details?: string; hooks: string[]; calls: string[]; error?: string}> { + const claude = entry.claude; + if (!claude || !PLUGIN_ID.test(claude.pluginId)) return {hooks: [], calls: [], error: 'Not a Claude plugin'}; + const env = claude.configDir ? {...process.env, CLAUDE_CONFIG_DIR: claude.configDir} : process.env; + const options = {cwd: claude.projectPath ?? process.cwd(), env, timeout: 30_000, maxBuffer: 512 * 1024, encoding: 'utf8' as const}; + let details: string | undefined; + try { details = displayText((await run(executable, ['plugin', 'details', claude.pluginId], options)).stdout).slice(0, 4000); } catch (error) { details = undefined; void error; } + const hooks: string[] = [], calls: string[] = []; + if (entry.components?.mod && entry.reference) { + try { + // `validate` lists a mod's hooks and API calls without running it (Claude's documented review step). + const {stdout} = await run(executable, ['plugin', 'validate', entry.reference], options).catch(error => ({stdout: (error as {stdout?: string}).stdout ?? ''})); + for (const line of stdout.split('\n')) { + // Per-module lines only ("❯ ./register.tsx hooks: …"), never the "Validating hooks: " header. + const hook = /\s\.{0,2}\/\S+\s+hooks:\s*(.+)$/u.exec(line)?.[1]; + const call = /\s\.{0,2}\/\S+\s+calls:\s*(.+)$/u.exec(line)?.[1]; + if (hook) hooks.push(...hook.split(/,\s*(?![^{]*\})/u).map(item => text(item, 120)).filter(Boolean)); + if (call) calls.push(...call.split(/,\s*/u).map(item => text(item, 80)).filter(Boolean)); + } + } catch { /* inspection is best effort */ } + } + return {...(details ? {details} : {}), hooks: hooks.slice(0, 64), calls: calls.slice(0, 64), ...(!details && !hooks.length ? {error: 'Claude could not describe this plugin'} : {})}; +} + +/** A mod whose only hooks draw (ui.render): nothing of it reaches a managed session. */ +export function drawsOnly(hooks: readonly string[]): boolean { + return hooks.length > 0 && hooks.every(hook => hook.startsWith('ui.')); +} + +/** Claude's plugin listing for one launch identity (passive; no marketplace refresh). */ +export async function listClaudePlugins(executable: string, cwd: string, configDir?: string): Promise { + const {stdout} = await run(executable, ['plugin', 'list', '--json'], {cwd, ...(configDir ? {env: {...process.env, CLAUDE_CONFIG_DIR: configDir}} : {}), timeout: 15_000, maxBuffer: 1024 * 1024, encoding: 'utf8'}); + return JSON.parse(stdout) as unknown; +} diff --git a/src/agents/mods/controller.ts b/src/agents/mods/controller.ts index 0ab51e8c..954c957d 100644 --- a/src/agents/mods/controller.ts +++ b/src/agents/mods/controller.ts @@ -4,7 +4,9 @@ import type {Key} from '../../terminal/keys.js'; import {cycleTab, tabCycleDelta} from '../../ui/PanelShell.js'; export const MOD_TABS = ['All', 'Portable', 'Provider-native', 'Context Packs'] as const; -export type ModAction = {kind: 'Search' | 'Back' | 'ActivateAction' | 'Refresh' | 'BackspaceSearch'} | {kind: 'TypeSearch'; text: string} | {kind: 'SelectTab' | 'Navigate'; direction: number} | {kind: 'ProviderNext'} | {kind: 'Scroll'; rows: number}; +export type ModAction = {kind: 'Search' | 'Back' | 'ActivateAction' | 'Refresh' | 'BackspaceSearch' | 'Toggle' | 'Inspect'} | {kind: 'TypeSearch'; text: string} | {kind: 'SelectTab' | 'Navigate'; direction: number} | {kind: 'ProviderNext'} | {kind: 'Scroll'; rows: number}; +/** What a static inspection found: Claude's component summary, and a mod's hooks and calls. */ +export interface ModInspection {details?: string; hooks: string[]; calls: string[]; error?: string} export class ModsController { private inventory: ModEntry[] = []; tab: 'All' | ModKind = 'All'; @@ -17,6 +19,15 @@ export class ModsController { refreshing = false; stale = false; message?: string; + /** A change waiting for Enter: turning the entry on or off through the provider's own command. */ + confirm?: {key: string; enable: boolean}; + /** A provider command in flight, in words ("Disabling skins…"). */ + busy?: string; + readonly inspections = new Map(); + get selectedEntry(): ModEntry | undefined {return this.rows[this.selected];} + /** Every entry, unfiltered (the host checks a change against the full listing). */ + get entries(): readonly ModEntry[] {return this.inventory;} + inventoryEntry(key: string | undefined): ModEntry | undefined {return key ? this.inventory.find(entry => entry.key === key) : undefined;} get providers(): string[] {return [...new Set(this.inventory.flatMap(i => [i.provider, ...i.applicability]))].filter(p => p !== 'nmsh').sort();} get rows(): ModEntry[] { const q = this.query.toLowerCase(); @@ -29,8 +40,26 @@ export class ModsController { this.inventory = items; this.selected = Math.max(0, this.rows.findIndex(i => i.key === key)); } - dispatch(action: ModAction): 'close' | 'refresh' | undefined { + dispatch(action: ModAction): 'close' | 'refresh' | 'toggle' | 'inspect' | undefined { + // While a provider command runs, keys wait: its result decides what the list shows next. + if (this.busy) return undefined; + if (this.confirm) { + // A pending change owns Enter and Esc; anything else cancels it so nothing changes by accident. + const pending = this.confirm; + this.confirm = undefined; + if (action.kind === 'ActivateAction' && this.selectedEntry?.key === pending.key) {this.confirm = pending; return 'toggle';} + if (action.kind !== 'Back') this.message = 'Change cancelled.'; + return undefined; + } switch (action.kind) { + case 'Toggle': { + const entry = this.selectedEntry; + if (!entry) break; + if (!entry.toggle?.supported) {this.message = entry.toggle ? `Can't toggle here: ${entry.toggle.reason}` : 'NMSh has no supported way to turn this on or off.'; break;} + if (entry.enabled === 'unknown') {this.message = 'Its current state is unknown, so NMSh will not change it.'; break;} + this.confirm = {key: entry.key, enable: entry.enabled === 'no'}; this.message = undefined; break; + } + case 'Inspect': if (this.selectedEntry) {this.details = true; this.scroll = 0; return 'inspect';} break; case 'Search': this.owner = 'SEARCH'; this.details = false; break; case 'TypeSearch': this.query += displayText(action.text).slice(0, Math.max(0, 256 - this.query.length)); this.selected = 0; break; case 'BackspaceSearch': this.query = [...this.query].slice(0, -1).join(''); this.selected = 0; break; @@ -66,6 +95,8 @@ export function modsKeyAction(key: Key, owner: 'PANEL' | 'SEARCH'): ModAction | if (delta) return {kind: 'SelectTab', direction: delta}; if (key.kind === 'enter') return {kind: 'ActivateAction'}; if (key.kind === 'text') { + if (key.value === ' ') return {kind: 'Toggle'}; + if (key.value.toLowerCase() === 'i') return {kind: 'Inspect'}; if (key.value === '/') return {kind: 'Search'}; if (key.value.toLowerCase() === 'r') return {kind: 'Refresh'}; if (key.value.toLowerCase() === 'p') return {kind: 'ProviderNext'}; diff --git a/src/agents/mods/inventory.ts b/src/agents/mods/inventory.ts index a09ef65d..8b98f933 100644 --- a/src/agents/mods/inventory.ts +++ b/src/agents/mods/inventory.ts @@ -3,10 +3,12 @@ import {firstPartyPacks} from '../../context/modules.js'; import {installedPackStatuses, readManifest} from '../../context/packs/store.js'; import {claudeSettingsPath} from '../claudeStatusLine.js'; import {resolveCommand} from '../../providers/providers.js'; -import {discoverClaudePlugins} from './claudeDiscovery.js'; +import {claudePluginEntries, listClaudePlugins, standaloneSkills} from './claudePlugins.js'; +import {homedir} from 'node:os'; import {builtinModInventory, type ModEntry} from './model.js'; import {displayText} from '../transcript/projection.js'; import type {AgentProfile} from '../sessions/manager.js'; +import type {AgentSession} from '../sessions/model.js'; export async function discoverHooks(paths: readonly {path: string; scope: 'global' | 'project'}[]): Promise { const entries: ModEntry[] = []; @@ -23,7 +25,9 @@ export async function discoverHooks(paths: readonly {path: string; scope: 'globa if (!hook || typeof hook !== 'object') return; const id = `${displayText(event).slice(0, 80)} hook ${index + 1}.${n + 1}`; entries.push({key: JSON.stringify(['claude-hook', path, event, index, n]), id, name: id, kind: 'Provider-native', provider: 'claude', source: 'Claude settings hook declaration', scope, - reference: displayText(path).slice(0, 512), installedBy: 'unknown', enabled: value.disableAllHooks === true ? 'no' : 'unknown', managed: false, executesInside: 'Claude Code', sandbox: 'No', permissions: [], applicability: ['claude'], evidence: 'Passive declaration only; effective provider settings/activation not inferred'}); + reference: displayText(path).slice(0, 512), installedBy: 'unknown', enabled: value.disableAllHooks === true ? 'no' : 'unknown', managed: false, executesInside: 'Claude Code', sandbox: 'No', permissions: [], applicability: ['claude'], evidence: 'Passive declaration only; effective provider settings/activation not inferred', + nativeType: 'Settings hook', runtime: 'Runs in NMSh-managed sessions like any Claude session', notes: [], + toggle: {supported: false, reason: 'Settings hooks live in Claude\'s settings files, which NMSh does not edit; disableAllHooks there turns them off'}}); }); }); } @@ -39,9 +43,26 @@ export async function loadModInventory(cwd: string, version: string, profiles: r const claudeProfiles = profiles.filter(p => p.harness === 'claude'); const namespaces = claudeProfiles.length ? claudeProfiles : [undefined]; for (const profile of namespaces) { - const native = executable ? await discoverClaudePlugins(executable, cwd, profile?.configDir) : []; + const configDir = profile?.configDir ?? process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'); + const native = executable ? await claudePluginEntries(await listClaudePlugins(executable, cwd, profile?.configDir), cwd, profile ? {name: profile.name, ...(profile.configDir ? {configDir: profile.configDir} : {})} : undefined) : []; + native.push(...await standaloneSkills(configDir)); native.push(...await discoverHooks([{path: profile?.configDir ? join(profile.configDir, 'settings.json') : claudeSettingsPath(), scope: 'global'}, {path: join(cwd, '.claude', 'settings.json'), scope: 'project'}, {path: join(cwd, '.claude', 'settings.local.json'), scope: 'project'}])); entries.push(...native.map(i => profile ? {...i, profileId: profile.name, key: JSON.stringify([profile.name, i.key])} : i)); } return entries; } + +/** + * Which running managed targets report each Claude plugin loaded (from their own init or reload report): the + * evidence that an enabled plugin is actually active there, not merely listed. + */ +export function withLoadedIn(entries: ModEntry[], sessions: readonly AgentSession[]): ModEntry[] { + const live = sessions.filter(session => session.harness === 'claude' && session.level === 'managed' && session.telemetry?.runtime); + return entries.map(entry => { + if (!entry.claude) return entry; + const name = entry.claude.pluginId.split('@')[0]; + const loadedIn = live.filter(session => (session.profileId ?? undefined) === (entry.profileId ?? undefined) && session.telemetry!.runtime!.plugins.some(plugin => plugin.name === name || plugin.name === entry.claude!.pluginId)) + .map(session => session.title); + return {...entry, loadedIn}; + }); +} diff --git a/src/agents/mods/model.ts b/src/agents/mods/model.ts index fa3c9d34..b0322e82 100644 --- a/src/agents/mods/model.ts +++ b/src/agents/mods/model.ts @@ -9,7 +9,22 @@ export interface ModEntry { executesInside: string; sandbox: 'No' | 'Not executable' | 'Unknown'; permissions: readonly string[]; applicability: readonly string[]; evidence: string; + /** The provider's own kind: a mod (code inside the provider), a plugin, a settings hook, a skill. */ + nativeType?: 'Mod' | 'Plugin' | 'Settings hook' | 'Skill'; + components?: ModComponents; + /** What the entry does in an NMSh-managed session, in words. */ + runtime?: string; + /** Conflicts, overrides, dependencies and options, in words. */ + notes?: readonly string[]; + /** Whether NMSh can turn it on or off through a supported provider command, and why not. */ + toggle?: {supported: true} | {supported: false; reason: string}; + /** Facts NMSh needs to change a Claude plugin through Claude's own command. */ + claude?: {pluginId: string; scope: 'user' | 'project' | 'local'; projectPath?: string; configDir?: string}; + /** Managed targets whose provider reported this plugin loaded (evidence that it is active there). */ + loadedIn?: readonly string[]; } + +export interface ModComponents {mod: boolean; hookEvents: number; skills: number; agents: number; commands: number; mcp: boolean; lsp: boolean} export const CONTEXT_METER = {id: 'context-meter', name: 'Context Meter', requested: ['context.status'], providers: ['claude', 'codex'], execution: 'inert'} as const; export function builtinModInventory(): ModEntry[] { return [{key: 'portable:context-meter', id: CONTEXT_METER.id, name: CONTEXT_METER.name, kind: 'Portable', provider: 'nmsh', diff --git a/src/agents/mods/view.ts b/src/agents/mods/view.ts index e4b2824d..409fee21 100644 --- a/src/agents/mods/view.ts +++ b/src/agents/mods/view.ts @@ -75,7 +75,7 @@ function row(entry: ModEntry, selected: boolean, columns: number): string { const stateWidth = wide ? 15 : 9; const nameWidth = columns >= 90 ? 40 : columns >= 60 ? Math.max(16, columns - 40) : Math.max(4, columns - 4 - stateWidth); const short = {on: 'on', off: 'off', unknown: 'unknown', inert: 'inert'} as const; - const meta = [SCOPE[entry.scope], entry.version ? clean(entry.version, 24) : ''].filter(Boolean).join(' · '); + const meta = [entry.nativeType ?? '', SCOPE[entry.scope], entry.version ? clean(entry.version, 24) : ''].filter(Boolean).join(' · '); const name = padCells(truncateText(clean(entry.name, 200), nameWidth - 2), nameWidth, 0); const stateText = padCells(wide ? state.text : short[state.tone], stateWidth, 0); const metaText = columns >= 60 ? meta : ''; @@ -89,12 +89,28 @@ function controls(state: ModsController, columns: number): string[] { const pairs: Array<[string, string]> = state.owner === 'SEARCH' ? [['type', 'filter'], ['Enter', 'details'], ['Esc', 'clear search']] : state.details - ? [[safe ? 'Up/Down' : '↑↓', 'scroll'], ['R', 'refresh'], ['Esc', 'back to list']] - : [[safe ? 'Up/Down' : '↑↓', 'select'], ['Tab', 'kind'], ...(state.rows.length ? [['Enter', 'details'] as [string, string]] : []), ['/', 'search'], ['P', 'provider'], ['R', 'refresh'], ['Esc', 'close']]; + ? [[safe ? 'Up/Down' : '↑↓', 'scroll'], ...(state.selectedEntry?.toggle?.supported ? [['Space', 'on/off'] as [string, string]] : []), ...(state.selectedEntry?.claude ? [['i', 'inspect'] as [string, string]] : []), ['R', 'refresh'], ['Esc', 'back to list']] + : state.confirm ? [['Enter', 'confirm'], ['Esc', 'cancel']] + : [[safe ? 'Up/Down' : '↑↓', 'select'], ['Tab', 'kind'], ...(state.rows.length ? [['Enter', 'details'] as [string, string]] : []), + ...(state.selectedEntry?.toggle?.supported ? [['Space', 'on/off'] as [string, string]] : []), ...(state.selectedEntry?.claude ? [['i', 'inspect'] as [string, string]] : []), + ['/', 'search'], ['P', 'provider'], ['R', 'refresh'], ['Esc', 'close']]; return renderControlRows(pairs, columns); } -function detail(entry: ModEntry, columns: number): string[] { +/** What turning the entry on or off would do, said before it happens; restart needs included. */ +export function changeWords(entry: ModEntry): string { + if (!entry.toggle) return 'Not available here; use the provider, or the Context Pack workflow for packs.'; + if (!entry.toggle.supported) return entry.toggle.reason; + const scope = entry.claude?.scope === 'user' ? 'user scope' : `${entry.claude?.scope ?? 'its'} scope`; + return `Space runs Claude's own claude plugin ${entry.enabled === 'yes' ? 'disable' : 'enable'} at ${scope}${entry.profileId ? ` for ${clean(entry.profileId, 40)}` : ''}. Running NMSh-managed sessions on that account reload plugins at once; Claude's own terminal sessions need /reload-plugins or a restart.`; +} + +/** The confirmation line for a pending change. */ +export function confirmWords(entry: ModEntry, enable: boolean): string { + return `${enable ? 'Enable' : 'Disable'} ${clean(entry.name, 80)}${entry.profileId ? ` for ${clean(entry.profileId, 40)}` : ''} at ${entry.claude?.scope ?? 'its'} scope? Enter confirms · Esc cancels`; +} + +function detail(entry: ModEntry, columns: number, inspection?: import('./controller.js').ModInspection): string[] { const c = color(); const width = Math.max(1, columns - 2 - FIELD); const field = (label: string, value: string, style = c.secondary) => wrapCells(value, width).map((line, index) => @@ -105,6 +121,20 @@ function detail(entry: ModEntry, columns: number): string[] { head, ...(entry.description ? wrapCells(clean(entry.description, 600), Math.max(1, columns - 2)).map(line => ` ${c.subtle}${line}${RESET}`) : []), '', + ...(entry.nativeType || entry.runtime ? [` ${c.accent}In NMSh${RESET}`, + ...(entry.nativeType ? field('Type', entry.nativeType === 'Mod' ? 'Mod: code that runs inside Claude Code' : entry.nativeType) : []), + ...(entry.components ? field('Contains', componentWords(entry.components)) : []), + ...(entry.runtime ? field('In sessions', clean(entry.runtime, 400)) : []), + ...field('Loaded in', entry.loadedIn?.length ? entry.loadedIn.map(item => clean(item, 60)).join(', ') : 'no running managed target reports it'), + ...(entry.notes ?? []).flatMap((note, index) => field(index ? '' : 'Notes', clean(note, 300))), + ...field('Change', changeWords(entry)), + ''] : []), + ...(inspection ? [` ${c.accent}Inspection${RESET}`, + ...(inspection.hooks.length ? field('Hooks', inspection.hooks.join(', ')) : []), + ...(inspection.calls.length ? field('Calls', inspection.calls.join(', ')) : []), + ...(inspection.details ? inspection.details.split('\n').filter(line => line.trim()).slice(0, 24).map(line => ` ${c.secondary}${truncateText(clean(line, 200), Math.max(1, columns - 2))}${RESET}`) : []), + ...(inspection.error ? field('Problem', inspection.error, c.failure) : []), + ''] : []), ` ${c.accent}Execution${RESET}`, ...field('Runs', entry.kind === 'Provider-native' ? `inside ${clean(entry.executesInside, 60)}, under the provider's own controls` : entry.kind === 'Portable' ? 'nowhere: an inert descriptor with no executable code' : 'nowhere: a declarative pack NMSh reads as data'), ...field('Sandbox', entry.kind === 'Provider-native' ? 'none from NMSh' : 'not applicable (not executable)', entry.kind === 'Provider-native' ? c.failure : c.secondary), @@ -120,11 +150,17 @@ function detail(entry: ModEntry, columns: number): string[] { ...(entry.reference ? field('Location', clean(entry.reference)) : []), ...field('Evidence', clean(entry.evidence)), ...field('Trust', 'unknown: listing or integrity is not trust'), - '', - ...wrapCells('Enabling or disabling is not available here; use the provider, or the Context Pack workflow for packs.', Math.max(1, columns - 2)).map(line => ` ${c.subtle}${line}${RESET}`), + ...(entry.toggle ? [] : ['', ...wrapCells(changeWords(entry), Math.max(1, columns - 2)).map(line => ` ${c.subtle}${line}${RESET}`)]), ]; } +function componentWords(components: import('./model.js').ModComponents): string { + const parts = [components.mod ? 'mod hooks' : '', components.skills ? `${components.skills} skill${components.skills === 1 ? '' : 's'}` : '', components.agents ? `${components.agents} agent${components.agents === 1 ? '' : 's'}` : '', + components.commands ? `${components.commands} command${components.commands === 1 ? '' : 's'}` : '', components.hookEvents ? `settings hooks on ${components.hookEvents} event${components.hookEvents === 1 ? '' : 's'}` : '', + components.mcp ? 'MCP server' : '', components.lsp ? 'LSP server' : ''].filter(Boolean); + return parts.length ? parts.join(', ') : 'nothing NMSh can read from its manifest'; +} + /** Word wrap in display cells; hard-breaks only words wider than the line. */ function wrapCells(text: string, width: number): string[] { const max = Math.max(1, width); @@ -155,7 +191,7 @@ export function renderMods(state: ModsController, columns: number, height: numbe const status = state.refreshing ? `${c.subtle}refreshing…${RESET}` : state.stale ? `${c.failure}stale${RESET}` : ''; // The subtitle shortens by whole clauses, never mid-phrase; the refresh state always keeps its place. const reserve = status ? displayWidth(status) + 2 : 0; - const subtitle = ['provider extensions and NMSh packs · inventory only; nothing runs from here', 'inventory only; nothing runs from here', ''] + const subtitle = ['provider extensions and NMSh packs · discovery runs nothing', 'discovery runs nothing', ''] .find(text => !text || 8 + displayWidth(text) + reserve <= width)!; const title = `${c.primary} Mods${RESET}${subtitle ? ` ${c.subtle}${subtitle}${RESET}` : ''}${status ? ` ${status}` : ''}`; const head = [title, @@ -172,7 +208,7 @@ export function renderMods(state: ModsController, columns: number, height: numbe let body: string[]; if (state.details && selected) { - const lines = detail(selected, width); + const lines = detail(selected, width, state.inspections.get(selected.key)); const room = Math.max(1, height - head.length - message.length - footer.length - 1); const offset = Math.min(state.scroll, Math.max(0, lines.length - room)); body = lines.slice(offset, offset + room); @@ -183,6 +219,9 @@ export function renderMods(state: ModsController, columns: number, height: numbe // Below the list: a more cue, then the selected entry's execution facts and evidence. // The execution facts wrap rather than truncate: they are never cut at narrow widths. Evidence is secondary. const summary = selected ? [ + ...(state.confirm?.key === selected.key ? wrapCells(confirmWords(selected, state.confirm.enable), Math.max(1, width - 2)).map(line => ` ${c.accent}${line}${RESET}`) : []), + ...(state.busy ? [` ${c.secondary}${truncateText(clean(state.busy, 200), Math.max(1, width - 2))}${RESET}`] : []), + ...(selected.runtime ? wrapCells(clean(selected.runtime, 300), Math.max(1, width - 2)).map(line => ` ${c.secondary}${line}${RESET}`) : []), ...wrapCells(modExecution(selected), Math.max(1, width - 2)).map(line => ` ${c.secondary}${line}${RESET}`), ` ${c.subtle}${truncateText(clean(selected.evidence), Math.max(1, width - 2))}${RESET}`, ] : []; diff --git a/src/agents/sessions/manager.ts b/src/agents/sessions/manager.ts index f3d2d570..a44d989b 100644 --- a/src/agents/sessions/manager.ts +++ b/src/agents/sessions/manager.ts @@ -207,6 +207,21 @@ export class AgentSessions { return this.control(id)?.mcpReconnect(name) ?? {ok: false, reason: 'This target has no live control channel'}; } + /** Reload plugins from disk into a running target (reload_plugins): what loaded, and how many failed to. */ + async reloadPlugins(id: string): Promise> { + const control = this.control(id); + if (!control) return {ok: false, reason: 'This target has no live control channel'}; + const result = await control.request({subtype: 'reload_plugins'}, 60_000); + if (!result.ok) return result; + const body = result.value as {plugins?: unknown; error_count?: unknown} | undefined; + const plugins = Array.isArray(body?.plugins) ? body!.plugins.flatMap(item => item && typeof item === 'object' && typeof (item as {name?: unknown}).name === 'string' ? [(item as {name: string}).name.slice(0, 96)] : []).slice(0, 256) : []; + const errors = typeof body?.error_count === 'number' && Number.isFinite(body.error_count) ? body.error_count : 0; + const session = this.get(id); + if (session?.telemetry?.runtime) session.telemetry.runtime = {...session.telemetry.runtime, plugins: plugins.map(name => ({name})), at: this.env.now()}; + if (session) this.emit(session); + return {ok: true, value: {plugins, errors}}; + } + async stopTask(id: string, taskId: string): Promise { return this.control(id)?.stopTask(taskId) ?? {ok: false, reason: 'This target has no live control channel'}; } diff --git a/src/app/TerminalApp.ts b/src/app/TerminalApp.ts index ba5d54bb..1d7a295c 100644 --- a/src/app/TerminalApp.ts +++ b/src/app/TerminalApp.ts @@ -309,7 +309,8 @@ import {attachRenderer, renderWorkspace, type RenderOptions as GithubRenderOptio import {safeGithubUrl} from '../githubWorkspace/sanitize.js'; import {GithubWorkspaceService} from '../githubWorkspace/service.js'; import {GhSource} from '../githubWorkspace/source.js'; -import {loadModInventory} from '../agents/mods/inventory.js'; +import {loadModInventory, withLoadedIn} from '../agents/mods/inventory.js'; +import {inspectPlugin, setPluginEnabled} from '../agents/mods/claudePlugins.js'; import {openEffortPicker, openModelPicker} from '../agents/input/pickers.js'; import {applyPickerChoice} from '../agents/input/surface.js'; import {settingsEffort} from '../agents/claudeSettings.js'; @@ -1596,6 +1597,8 @@ export class TerminalApp { const result = action ? this.modsPanel.dispatch(action) : undefined; if (result === 'close') {this.modsPanel = undefined; this.returnFromPanel();} else if (result === 'refresh') void this.refreshMods(); + else if (result === 'toggle') void this.toggleMod(); + else if (result === 'inspect') void this.inspectMod(); this.render(); return; } if (this.worktreePanel) { void this.handleWorktreeKey(key); return; } @@ -8287,10 +8290,59 @@ export class TerminalApp { private async refreshMods(): Promise { const panel = this.modsPanel; if (!panel) return; - await panel.refresh(() => loadModInventory(this.shellCwd, this.buildIdentity.version, this.promptConfiguration.agentProfiles)); + await panel.refresh(async () => withLoadedIn(await loadModInventory(this.shellCwd, this.buildIdentity.version, this.promptConfiguration.agentProfiles), this.agents.sessions)); if (this.modsPanel === panel) this.render(); } + /** + * The confirmed change, through Claude's own command at the plugin's installed scope; then the listing is read again + * as evidence, and running managed targets of the same launch identity reload plugins. Says exactly what happened. + */ + private async toggleMod(): Promise { + const panel = this.modsPanel; + const pending = panel?.confirm; + const entry = panel?.inventoryEntry(pending?.key); + if (!panel || !pending || !entry) return; + panel.confirm = undefined; + const executable = resolveCommand('claude'); + if (!executable) { panel.message = 'Claude Code is not on PATH.'; this.render(); return; } + panel.busy = `${pending.enable ? 'Enabling' : 'Disabling'} ${entry.name}…`; + this.render(); + try { + const installed = new Set(panel.entries.filter(item => item.profileId === entry.profileId && item.claude).map(item => item.claude!.pluginId)); + const result = await setPluginEnabled(executable, entry, pending.enable, installed); + if (!result.ok) { panel.message = `Not changed: ${result.message}`; return; } + await panel.refresh(async () => withLoadedIn(await loadModInventory(this.shellCwd, this.buildIdentity.version, this.promptConfiguration.agentProfiles), this.agents.sessions)); + const after = panel.inventoryEntry(pending.key); + const confirmed = after?.enabled === (pending.enable ? 'yes' : 'no'); + const targets = this.agents.sessions.filter(session => session.harness === 'claude' && session.level === 'managed' && (session.profileId ?? undefined) === (entry.profileId ?? undefined) + && this.agents.controllable(session.id) && (entry.claude?.scope === 'user' || session.cwd === entry.claude?.projectPath)); + const reloads = await Promise.all(targets.map(async session => ({session, result: await this.agents.reloadPlugins(session.id)}))); + const reloaded = reloads.filter(item => item.result.ok); + const errors = reloaded.reduce((sum, item) => sum + (item.result.ok ? item.result.value.errors : 0), 0); + panel.message = [ + `${entry.name} ${pending.enable ? 'enabled' : 'disabled'}${result.alreadyInGoalState ? ' (it already was)' : ''}${confirmed ? ', confirmed by Claude\'s listing' : ', but Claude\'s listing does not show it yet'}.`, + targets.length ? `Reloaded in ${reloaded.length} of ${targets.length} running managed session${targets.length === 1 ? '' : 's'}${errors ? `; Claude reported ${errors} plugin load error${errors === 1 ? '' : 's'}` : ''}.` : 'New sessions pick it up; no managed session on this account is running.', + 'Claude\'s own terminal sessions need /reload-plugins or a restart.', + ].join(' '); + if (this.modsPanel === panel) panel.setInventory(withLoadedIn([...panel.entries], this.agents.sessions)); + } finally { + panel.busy = undefined; + if (this.modsPanel === panel) this.render(); + } + } + + private async inspectMod(): Promise { + const panel = this.modsPanel; + const entry = panel?.selectedEntry; + const executable = resolveCommand('claude'); + if (!panel || !entry?.claude || !executable || panel.inspections.has(entry.key)) return; + panel.busy = `Asking Claude to describe ${entry.name}…`; + this.render(); + try { panel.inspections.set(entry.key, await inspectPlugin(executable, entry)); } + finally { panel.busy = undefined; if (this.modsPanel === panel) this.render(); } + } + /** /copy inside an agent view: the Nth newest reply's visible text, never protocol data. */ private async copyAgentBlock(session: AgentSession, index: number): Promise { const replies = agentBlocks(session).filter(block => block.kind === 'assistant'); diff --git a/tests/claudePlugins.test.ts b/tests/claudePlugins.test.ts new file mode 100644 index 00000000..18534f7d --- /dev/null +++ b/tests/claudePlugins.test.ts @@ -0,0 +1,207 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync} from 'node:fs'; +import {tmpdir} from 'node:os'; +import {join} from 'node:path'; +import {spawnSync} from 'node:child_process'; +import {claudePluginEntries, drawsOnly, inspectPlugin, managedRuntime, readComponents, setPluginEnabled, standaloneSkills} from '../src/agents/mods/claudePlugins.js'; +import {withLoadedIn} from '../src/agents/mods/inventory.js'; +import {ModsController, modsKeyAction} from '../src/agents/mods/controller.js'; +import {renderMods} from '../src/agents/mods/view.js'; +import type {ModEntry} from '../src/agents/mods/model.js'; +import type {AgentSession} from '../src/agents/sessions/model.js'; +import {emptyTelemetry} from '../src/agents/telemetry.js'; +import {setIconStyle} from '../src/ui/glyphs.js'; +import {displayWidth, stripAnsi} from '../src/util/text.js'; +import type {Key} from '../src/terminal/keys.js'; + +const root = mkdtempSync(join(tmpdir(), 'nmsh-plugins-')); +test.after(() => rmSync(root, {recursive: true, force: true})); +const write = (path: string, content: string) => { mkdirSync(join(path, '..'), {recursive: true}); writeFileSync(path, content); }; + +/** Two launch identities, each with its own plugins folder, as claude-account1 and claude-account2 are laid out. */ +function plugin(account: string, name: string, files: Record): string { + const dir = join(root, account, 'plugins', 'cache', 'market', name, '1.0.0'); + for (const [file, content] of Object.entries(files)) write(join(dir, file), content); + return dir; +} +const skins = plugin('account1', 'skins', {'.claude-plugin/plugin.json': JSON.stringify({name: 'skins', version: '1.0.0', description: 'Themed transcript rows'}), 'hooks/hooks.json': '{"modules": ["./register.tsx"]}', + 'hooks/register.tsx': 'throw new Error("discovery must never run plugin code")', 'skills/skin-designer/SKILL.md': '---\ndescription: d\n---'}); +const guard = plugin('account1', 'guard', {'.claude-plugin/plugin.json': JSON.stringify({name: 'guard', dependencies: ['base@market']}), 'hooks/hooks.json': JSON.stringify({hooks: {PreToolUse: [], Stop: []}}), + 'agents/reviewer.md': '#', 'commands/check.md': '#', '.mcp.json': '{"mcpServers": {}}'}); +const project = join(root, 'project'); +write(join(project, '.claude', 'settings.local.json'), JSON.stringify({enabledPlugins: {'guard@market': false}})); +const LISTING = [ + {id: 'skins@market', scope: 'user', version: '1.0.0', enabled: true, installPath: skins, projectEnabled: false}, + {id: 'guard@market', scope: 'user', version: '1.0.0', enabled: true, installPath: guard, projectEnabled: false}, + {id: 'guard@market', scope: 'project', version: '0.9.0', enabled: true, installPath: guard, projectPath: project}, + {id: 'elsewhere@market', scope: 'project', enabled: true, projectPath: '/some/other/project'}, + {id: '../../etc@x', scope: 'user', enabled: true}, +]; + +test('components come from manifest files only; plugin code is never run', async () => { + const mod = await readComponents(skins); + assert.deepEqual(mod.components, {mod: true, hookEvents: 0, skills: 1, agents: 0, commands: 0, mcp: false, lsp: false}); + assert.equal(mod.description, 'Themed transcript rows'); + const hooks = await readComponents(guard); + assert.deepEqual(hooks.components, {mod: false, hookEvents: 2, skills: 0, agents: 1, commands: 1, mcp: true, lsp: false}); + assert.deepEqual(hooks.dependencies, ['base@market']); + assert.match(managedRuntime(mod.components), /^Mod hooks run in NMSh-managed sessions; panes, bands and restyled rows it draws do not appear there; 1 skill load$/u); + assert.match(managedRuntime(mod.components, true), /nothing of it appears in NMSh-managed sessions/u); + assert.ok(drawsOnly(['ui.render{component=Pane}']) && !drawsOnly(['tool.call{tool=Bash}', 'ui.render{component=Pane}'])); +}); + +test('entries: types, this project only, effective state with project overrides, conflicts and dependencies in words', async () => { + const entries = await claudePluginEntries(LISTING, project, {name: 'account1', configDir: join(root, 'account1')}); + assert.deepEqual(entries.map(entry => [entry.id, entry.nativeType, entry.scope, entry.enabled]), + [['skins@market', 'Mod', 'global', 'yes'], ['guard@market', 'Plugin', 'global', 'no'], ['guard@market', 'Plugin', 'project', 'yes']], + 'another project\'s installation is not shown; an unsafe id is dropped'); + const user = entries[1]!; + assert.match(user.notes!.join('\n'), /Disabled for this project by \.claude\/settings\.local\.json \(user setting: enabled\)/u); + assert.match(user.notes!.join('\n'), /Also installed at project scope \(version 0\.9\.0\)/u); + assert.match(user.notes!.join('\n'), /Depends on base@market/u); + assert.deepEqual(user.toggle, {supported: false, reason: 'This project\'s .claude/settings.local.json decides it here; NMSh changes only the scope a plugin is installed at'}); + assert.deepEqual(entries[0]!.toggle, {supported: true}); + assert.deepEqual(entries[0]!.claude, {pluginId: 'skins@market', scope: 'user', configDir: join(root, 'account1')}); + assert.equal(entries[0]!.sandbox, 'No', 'never presented as sandboxed by NMSh'); + await assert.rejects(claudePluginEntries({not: 'a list'}, project), /unsupported shape/u); +}); + +test('standalone skills are listed with no fake switch', async () => { + write(join(root, 'account2', 'skills', 'brag', 'SKILL.md'), '---\nname: brag\ndescription: "Turn a project into a launch video"\n---\n'); + write(join(root, 'account2', 'skills', 'synced', 'x', 'SKILL.md'), '---\n---'); + const skills = await standaloneSkills(join(root, 'account2'), 'account2'); + assert.deepEqual(skills.map(entry => [entry.name, entry.nativeType, entry.description, entry.toggle?.supported]), [['brag', 'Skill', 'Turn a project into a launch video', false]]); +}); + +/* A stand-in for Claude's own plugin command: records argv, CLAUDE_CONFIG_DIR and cwd; answers in the 2.1.295 JSON shape. */ +const fake = join(root, 'claude'); +const log = join(root, 'calls.jsonl'); +writeFileSync(fake, `#!/usr/bin/env node +const fs = require('fs'); +const args = process.argv.slice(2); +fs.appendFileSync(${JSON.stringify(log)}, JSON.stringify({args, configDir: process.env.CLAUDE_CONFIG_DIR, cwd: process.cwd()}) + '\\n'); +if (args[0] === 'plugin' && (args[1] === 'enable' || args[1] === 'disable')) { + if (args[2] === 'skins@market' && args[1] === 'enable') { console.log(JSON.stringify({command: 'enable', outcome: 'failed', plugin: args[2], scope: 'user', message: 'Plugin "skins@market" is already enabled at user scope', failureCode: 'already_in_goal_state', alreadyInGoalState: true})); process.exit(1); } + if (args[2] === 'broken@market') { console.log(JSON.stringify({command: args[1], outcome: 'failed', plugin: args[2], scope: 'user', message: 'Plugin not found'})); process.exit(1); } + console.log(JSON.stringify({command: args[1], outcome: 'ok', plugin: args[2], pluginId: args[2], scope: args[4], message: 'Successfully ' + args[1] + 'd plugin'})); +} else if (args[0] === 'plugin' && args[1] === 'details') { + console.log('skins 1.0.0\\n Description: Themed transcript rows\\nComponent inventory\\n Skills (1) skin-designer\\nProjected token cost\\n Always-on: ~115 tok'); +} else if (args[0] === 'plugin' && args[1] === 'validate') { + console.log('Validating hooks: x\\n ❯ ./register.tsx hooks: tool.call{tool=Bash}, ui.render{component=Pane}\\n ❯ ./register.tsx calls: $.ui.open, $.process.run\\n✔ Validation passed'); +} +`); +chmodSync(fake, 0o755); +const calls = () => existsSync(log) ? readFileSync(log, 'utf8').trim().split('\n').map(line => JSON.parse(line) as {args: string[]; configDir?: string; cwd: string}) : []; + +test('toggle: Claude\'s own command at the installed scope and account; unknown ids are never sent', async () => { + const entries = await claudePluginEntries(LISTING, project, {name: 'account1', configDir: join(root, 'account1')}); + const installed = new Set(entries.map(entry => entry.claude!.pluginId)); + const skinsEntry = entries[0]!; + assert.deepEqual(await setPluginEnabled(fake, skinsEntry, false, installed), {ok: true, message: 'Successfully disabled plugin'}); + const disable = calls().at(-1)!; + assert.deepEqual(disable.args, ['plugin', 'disable', 'skins@market', '--scope', 'user', '--json']); + assert.equal(disable.configDir, join(root, 'account1'), 'the change goes to that account\'s configuration, not the default one'); + assert.deepEqual(await setPluginEnabled(fake, skinsEntry, true, installed), {ok: true, message: 'Plugin "skins@market" is already enabled at user scope', alreadyInGoalState: true}); + const projectEntry = entries[2]!; + await setPluginEnabled(fake, projectEntry, false, installed); + assert.deepEqual(calls().at(-1)!.args.slice(3), ['--scope', 'project', '--json']); + assert.equal(calls().at(-1)!.cwd.replace(/^\/private/u, ''), project.replace(/^\/private/u, ''), 'project scope runs in that project'); + const before = calls().length; + // Claude's own enable accepts an id that is not installed and writes it into settings; NMSh never sends one. + const ghost: ModEntry = {...skinsEntry, claude: {...skinsEntry.claude!, pluginId: 'nope@nowhere'}}; + assert.deepEqual(await setPluginEnabled(fake, ghost, true, installed), {ok: false, message: 'Not an installed Claude plugin in the current listing'}); + assert.equal(calls().length, before, 'no command ran'); + const broken: ModEntry = {...skinsEntry, claude: {...skinsEntry.claude!, pluginId: 'broken@market'}}; + assert.deepEqual(await setPluginEnabled(fake, broken, true, new Set(['broken@market'])), {ok: false, message: 'Plugin not found'}, "Claude's refusal is reported as it worded it"); +}); + +test('inspect: Claude\'s own static descriptions; a mod\'s hooks and calls are listed', async () => { + const [skinsEntry] = await claudePluginEntries(LISTING, project, {name: 'account1', configDir: join(root, 'account1')}); + const inspection = await inspectPlugin(fake, skinsEntry!); + assert.match(inspection.details ?? '', /Always-on: {3}~115 tok/u); + assert.deepEqual(inspection.hooks, ['tool.call{tool=Bash}', 'ui.render{component=Pane}']); + assert.deepEqual(inspection.calls, ['$.ui.open', '$.process.run']); + assert.deepEqual(calls().at(-1)!.args, ['plugin', 'validate', skins]); +}); + +test('loaded in: only managed targets of the same account whose own report names the plugin', async () => { + const entries = await claudePluginEntries(LISTING, project, {name: 'account1', configDir: join(root, 'account1')}); + const target = (title: string, profileId: string | undefined, plugins: string[]): AgentSession => { + const telemetry = emptyTelemetry(); + telemetry.runtime = {tools: [], mcpServers: [], slashCommands: [], skills: [], plugins: plugins.map(name => ({name})), capabilities: [], at: 1}; + return {id: title, title, harness: 'claude', level: 'managed', ...(profileId ? {profileId} : {}), cwd: '/w', startedAt: 0, state: 'waiting', events: [], attention: false, updatedAt: 0, telemetry}; + }; + const marked = withLoadedIn(entries, [target('Fix login', 'account1', ['skins']), target('Other account', 'account2', ['skins']), target('Without it', 'account1', [])]); + assert.deepEqual(marked[0]!.loadedIn, ['Fix login']); + assert.deepEqual(marked[1]!.loadedIn, []); +}); + +const key = (kind: string, value = ''): Key => ({kind, value} as unknown as Key); + +test('keyboard: Space proposes, Enter confirms, any other key cancels; unsupported entries explain instead of toggling', async () => { + const entries = await claudePluginEntries(LISTING, project, {name: 'account1', configDir: join(root, 'account1')}); + const c = new ModsController(); + c.setInventory(entries); + c.tab = 'Provider-native'; + const press = (k: Key) => { const action = modsKeyAction(k, c.owner); return action ? c.dispatch(action) : undefined; }; + assert.equal(c.selectedEntry?.id, 'skins@market'); + assert.equal(press(key('text', ' ')), undefined); + assert.deepEqual(c.confirm, {key: entries[0]!.key, enable: false}); + assert.match(stripAnsi(renderMods(c, 100, 30).join('\n')), /Disable skins for account1 at user scope\? Enter confirms · Esc cancels/u); + assert.match(stripAnsi(renderMods(c, 100, 30).at(-1)!), /^Enter confirm · Esc cancel$/u); + assert.equal(press(key('down')), undefined, 'moving cancels'); + assert.equal(c.confirm, undefined); + assert.equal(c.message, 'Change cancelled.'); + assert.equal(c.selectedEntry?.id, 'skins@market', 'the cancelling key does nothing else'); + press(key('text', ' ')); + assert.equal(press(key('enter')), 'toggle'); + c.confirm = undefined; + press(key('down')); + press(key('text', ' ')); + assert.equal(c.confirm, undefined); + assert.match(c.message ?? '', /^Can't toggle here: This project's \.claude\/settings\.local\.json decides it here/u); + c.busy = 'Disabling skins…'; + assert.equal(press(key('escape')), undefined, 'keys wait while Claude runs the command'); + c.busy = undefined; + assert.equal(press(key('text', 'i')), 'inspect'); +}); + +test('rendering: every width fits; NO_COLOR and Safe glyphs keep type, state and the runtime truth in words', async () => { + const entries = await claudePluginEntries(LISTING, project, {name: 'account1', configDir: join(root, 'account1')}); + const c = new ModsController(); + c.setInventory(entries); + c.tab = 'Provider-native'; + for (const columns of [30, 44, 60, 100, 160]) for (const details of [false, true]) { + c.details = details; + const rows = renderMods(c, columns, 28); + assert.ok(rows.every(row => displayWidth(stripAnsi(row)) <= columns), `${columns} ${details}`); + assert.match(stripAnsi(rows.at(-1)!), /Esc/u); + } + c.details = false; + assert.match(stripAnsi(renderMods(c, 100, 28).join('\n')), /skins +enabled +Mod · global · 1\.0\.0/u); + c.details = true; + const detail = stripAnsi(renderMods(c, 100, 60).join('\n')); + assert.match(detail, /Type +Mod: code that runs inside Claude Code/u); + assert.match(detail, /In sessions +Mod hooks run in NMSh-managed sessions; panes, bands and restyled rows it draws do/u); + assert.match(detail, /Sandbox +none from NMSh/u); + assert.match(detail, /Change +Space runs Claude's own claude plugin disable at user scope for account1\./u); + const before = process.env.NO_COLOR; + setIconStyle('safe'); + process.env.NO_COLOR = '1'; + try { + c.details = false; + const plain = renderMods(c, 80, 28).join('\n'); + assert.doesNotMatch(plain, /\u001b\[(?:38|48);/u); + assert.doesNotMatch(stripAnsi(plain), /[↑↓›▏]/u); + assert.match(stripAnsi(plain), /skins +enabled/u); + } finally { setIconStyle('nerd'); if (before === undefined) delete process.env.NO_COLOR; else process.env.NO_COLOR = before; } +}); + +test('the real installed Claude, read-only: every listed plugin parses, with a type and a state (skipped without claude)', {skip: spawnSync('sh', ['-c', 'command -v claude'], {encoding: 'utf8'}).status !== 0 || !process.env.NMSH_REAL_CLAUDE ? 'set NMSH_REAL_CLAUDE=1 to read the real accounts' : false}, async () => { + const {listClaudePlugins} = await import('../src/agents/mods/claudePlugins.js'); + for (const configDir of (process.env.NMSH_REAL_CLAUDE_DIRS ?? '').split(':').filter(Boolean)) { + const entries = await claudePluginEntries(await listClaudePlugins('claude', process.cwd(), configDir), process.cwd(), {name: configDir, configDir}); + assert.ok(entries.every(entry => entry.nativeType && entry.enabled !== 'unknown'), configDir); + } +}); diff --git a/tests/modsMenu.test.ts b/tests/modsMenu.test.ts index 46f194e3..6245ba31 100644 --- a/tests/modsMenu.test.ts +++ b/tests/modsMenu.test.ts @@ -31,7 +31,7 @@ const plain = (lines: string[]) => lines.map(line => stripAnsi(line)); test('browse view: native tab strip, search and provider row, grouped rows with factual state words', () => { const lines = plain(renderMods(controller(), 100, 30)); const text = lines.join('\n'); - assert.match(lines[0]!, /^ {2}Mods {2}provider extensions and NMSh packs · inventory only; nothing runs from here$/u); + assert.match(lines[0]!, /^ {2}Mods {2}provider extensions and NMSh packs · discovery runs nothing$/u); assert.match(lines[1]!, /^ {3}All {3}Portable {3}Provider-native {3}Context Packs $/u); assert.match(lines[2]!, /^ {2}Search {2}\/ to filter {3}Provider {2}All$/u); assert.match(text, /^ {2}Portable descriptors$/mu); @@ -41,7 +41,9 @@ test('browse view: native tab strip, search and provider row, grouped rows with assert.doesNotMatch(text, /\n\n\n/u, 'no doubled blank rows'); assert.match(text, /^ {4}code-review@official +enabled +global · 1\.4\.0$/mu); assert.match(text, /^ {4}commit-commands@official +disabled +global · 1\.0\.2$/mu); - assert.match(text, /^ {4}project-lint@team +disabled +this project$/mu, 'projectEnabled is the effective state'); + // projectEnabled only says whether project settings name a plugin (it is false for nearly every installed one in + // Claude Code 2.1.295), so with no `enabled` the state is unknown, never inferred from it. + assert.match(text, /^ {4}project-lint@team +state unknown +this project$/mu, 'projectEnabled is not the effective state'); assert.match(text, /^ {4}Node\.js +state unknown +global · 1$/mu, 'unknown is never shown as enabled'); }); @@ -133,9 +135,9 @@ test('every width fits in display cells; narrow rows keep name and state; Safe g if (!details) assert.match(text, /commit-command\S*\s+(?:off|disabled)/u, 'state survives narrow rows'); } } - // The subtitle shortens by clause at 80 columns instead of being cut. - assert.equal(stripAnsi(renderMods(controller(), 80, 30)[0]!), ' Mods inventory only; nothing runs from here'); - assert.equal(stripAnsi(renderMods(controller(), 30, 30)[0]!), ' Mods'); + // The subtitle shortens by clause at 60 columns instead of being cut. + assert.equal(stripAnsi(renderMods(controller(), 60, 30)[0]!), ' Mods discovery runs nothing'); + assert.equal(stripAnsi(renderMods(controller(), 24, 30)[0]!), ' Mods'); setIconStyle('safe'); try { const details = controller(); details.dispatch({kind: 'ActivateAction'}); From 7a88a58577a09d828d29a13f1e171b8dc2c30426 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:49:56 +0530 Subject: [PATCH 009/249] Agent view: a right panel of what the target is doing to its context and the project From telemetry already collected (nothing read on the render path): context pressure with its source and age (Claude's own count with headroom to auto-compact and the largest categories, or an estimate from the last turn, never 0% when unreported), model, effort and permission mode with provenance, the plan when Claude emits one, running subagents and tasks, files read and edited with their line changes, compactions, usage limits only once close, and the session's estimated cost. It sits beside the conversation from 120 columns; /panel shows or hides it (72 columns at least), and /context asks Claude for its breakdown. Safe glyphs draw ASCII marks and bars; NO_COLOR keeps every distinction in words. --- src/agents/input/surface.ts | 10 ++ src/agents/sessions/AgentViews.ts | 2 + src/agents/workspace/sidePanel.ts | 173 ++++++++++++++++++++++++++++++ src/app/TerminalApp.ts | 25 ++++- tests/agentSidePanel.test.ts | 100 +++++++++++++++++ 5 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 src/agents/workspace/sidePanel.ts create mode 100644 tests/agentSidePanel.test.ts diff --git a/src/agents/input/surface.ts b/src/agents/input/surface.ts index 16890b8e..03840283 100644 --- a/src/agents/input/surface.ts +++ b/src/agents/input/surface.ts @@ -19,6 +19,10 @@ export interface AgentSurfaceHost { openPicker?(session: AgentSession, kind: PickerKind): PickerOpen; /** Apply one picker row through the provider's control channel; the host re-renders when it settles. */ applyPicker?(session: AgentSession, picker: PickerState, row: PickerRow): void; + /** Show or hide the right panel; returns what the person sees, in words. */ + togglePanel?(view: AgentViewState): string; + /** Ask the provider for its own context breakdown (shown in the right panel when it arrives). */ + refreshContext?(session: AgentSession): void; } /** `/model`, `/model sonnet`, `/effort`, `/effort high`: NMSh's own commands in a managed agent composer. A leading space sends the text to the agent verbatim. */ @@ -110,6 +114,12 @@ export function handleAgentInput(view: AgentViewState, session: AgentSession, ke else view.message = 'No pending permission request.'; return false; } + if (/^\/panel[ \t]*$/u.test(text)) { view.message = host.togglePanel?.(view) ?? 'The panel is not available here.'; c.editor.clear(); view.input = ''; return false; } + if (/^\/context[ \t]*$/u.test(text)) { + c.editor.clear(); view.input = ''; + if (host.refreshContext) { host.refreshContext(session); view.panel = true; view.message = 'Asking Claude for its context breakdown…'; } else view.message = '/context needs a running managed Claude target.'; + return false; + } const command = pickerCommand(text); if (command) { const opened = host.openPicker?.(session, command.kind) ?? {ok: false as const, reason: `/${command.kind} is not available for this target.`}; diff --git a/src/agents/sessions/AgentViews.ts b/src/agents/sessions/AgentViews.ts index 4e93992f..e2e2a447 100644 --- a/src/agents/sessions/AgentViews.ts +++ b/src/agents/sessions/AgentViews.ts @@ -145,6 +145,8 @@ export interface AgentViewState { seenRows?: number; message?: string; controller?: import('../input/controller.js').AgentInputController; + /** The right panel: the person's explicit choice (/panel); undefined follows the terminal width. */ + panel?: boolean; focusedObject?: import('../transcript/projection.js').SemanticObject; } diff --git a/src/agents/workspace/sidePanel.ts b/src/agents/workspace/sidePanel.ts new file mode 100644 index 00000000..ae7af535 --- /dev/null +++ b/src/agents/workspace/sidePanel.ts @@ -0,0 +1,173 @@ +import type {AgentSession} from '../sessions/model.js'; +import {contextMeter, effortStatus, formatTokens, modelName, currentModel, type AgentTelemetry} from '../telemetry.js'; +import {displayText} from '../transcript/projection.js'; +import {displayWidth, padCells, stripAnsi, truncateAnsi, truncateText} from '../../util/text.js'; +import {getCurrentGlyphMode} from '../../ui/glyphs.js'; +import {foreground, UI_COLORS} from '../../ui/palette.js'; +import {colorLevel} from '../../presentation/capabilities.js'; + +/** + * The agent view's right panel: what the target is doing to its context and the project, from telemetry only (never + * disk or the provider on the render path). Sections appear only when they have facts: context pressure with its + * source and age, model and effort, the plan, files read and edited, running subagents and tasks, compactions, and a + * rate limit only once it is close. Meaning never depends on color; every mark has words or an ASCII form. + */ + +const RESET = '\u001b[0m'; +/** Columns at which the panel shows by default; narrower terminals keep the conversation full width. */ +export const SIDE_PANEL_MIN_COLUMNS = 120; + +export function sidePanelWidth(columns: number): number { + return Math.max(30, Math.min(46, Math.floor(columns * 0.3))); +} + +/** Whether the panel is shown: the person's explicit choice, else on at wide terminals. */ +export function sidePanelShown(choice: boolean | undefined, columns: number): boolean { + return columns >= 72 && (choice ?? columns >= SIDE_PANEL_MIN_COLUMNS); +} + +const safe = () => getCurrentGlyphMode() === 'safe'; +const paint = (color: Parameters[0]) => colorLevel() === 'none' ? '' : foreground(color); + +function age(at: number, now: number): string { + const seconds = Math.max(0, Math.round((now - at) / 1000)); + if (seconds < 10) return 'now'; + if (seconds < 60) return `${seconds}s ago`; + const minutes = Math.round(seconds / 60); + return minutes < 60 ? `${minutes}m ago` : `${Math.round(minutes / 60)}h ago`; +} + +/** A filled bar in cells; Safe glyphs draw it with # and -. */ +function bar(percent: number, cells: number): string { + const filled = Math.max(0, Math.min(cells, Math.round((percent / 100) * cells))); + const [on, off] = safe() ? ['#', '-'] : ['█', '░']; + return on.repeat(filled) + off.repeat(cells - filled); +} + +function relative(path: string, cwd: string | undefined): string { + const clean = displayText(path); + if (cwd && clean.startsWith(`${cwd}/`)) return clean.slice(cwd.length + 1); + return clean.replace(/^\/(?:Users|home)\/[^/]+/u, '~'); +} + +export function renderSidePanel(session: AgentSession, width: number, height: number, now: number, settingsEffort?: string): string[] { + const telemetry = session.telemetry; + const primary = paint(UI_COLORS.primary), secondary = paint(UI_COLORS.secondary), subtle = paint(UI_COLORS.subtle), accent = paint(UI_COLORS.accent), failure = paint(UI_COLORS.failure); + const out: string[] = []; + const line = (text: string, style = secondary) => out.push(`${style}${truncateText(text, width)}${RESET}`); + const title = (text: string) => { if (out.length) out.push(''); out.push(`${accent}${truncateText(text, width)}${RESET}`); }; + if (!telemetry) { line('Nothing reported yet.', subtle); return fit(out, width, height); } + + context(telemetry, width, now, title, line, {primary, secondary, subtle, failure}); + const running = currentModel(telemetry, session.model); + if (running) { + title('Model'); + line(`${modelName(running.value, telemetry)}${running.source === 'requested' ? ' (requested)' : ''}`, primary); + // Compact provenance words: the panel is narrow, and the picker and header carry the full sentence. + const effort = effortStatus(telemetry, settingsEffort); + const words: Record = {acknowledged: `Effort ${effort.value} · acknowledged`, launch: `Effort ${effort.value} · launch flag`, settings: `Effort ${effort.value} · settings`, + default: 'Effort: model default', unavailable: 'No effort levels'}; + if (words[effort.state]) line(words[effort.state]!, subtle); + const mode = telemetry.requested.permissionMode?.value ?? telemetry.runtime?.permissionMode; + if (mode) line(`Permissions ${mode}${telemetry.requested.permissionMode ? ' (requested)' : ''}`, subtle); + } + + const todos = telemetry.todos?.value ?? []; + if (todos.length) { + const done = todos.filter(item => item.status === 'completed').length; + title(`Plan ${done}/${todos.length}`); + for (const item of todos.slice(0, 8)) { + const mark = item.status === 'completed' ? (safe() ? '[x]' : '✓') : item.status === 'in_progress' ? (safe() ? '[>]' : '▸') : (safe() ? '[ ]' : '·'); + line(`${mark} ${displayText(item.status === 'in_progress' && item.activeForm ? item.activeForm : item.content)}`, item.status === 'in_progress' ? primary : item.status === 'completed' ? subtle : secondary); + } + if (todos.length > 8) line(`+${todos.length - 8} more`, subtle); + } + + const tasks = [...telemetry.tasks.values()].filter(task => task.status === 'running' || now - task.updated < 30_000).reverse(); + if (tasks.length) { + title(`Subagents and tasks`); + for (const task of tasks.slice(0, 6)) { + const who = displayText(task.subagent ?? task.type ?? 'task'); + const state = task.status === 'running' ? `${Math.max(0, Math.round((now - task.started) / 1000))}s` : displayText(task.status); + line(`${who} · ${displayText(task.description)}`, task.status === 'running' ? primary : subtle); + line(` ${state}${task.tokens ? ` · ${formatTokens(task.tokens)} tok` : ''}${task.lastTool ? ` · ${displayText(task.lastTool)}` : ''}`, task.status === 'failed' ? failure : subtle); + } + } + + const files = [...telemetry.files.values()].reverse(); + if (files.length) { + const edited = files.filter(file => file.edits || file.writes); + title(`Files ${edited.length ? `${edited.length} changed · ` : ''}${files.length} touched`); + for (const file of files.slice(0, 10)) { + const change = file.edits || file.writes ? `+${file.added} -${file.removed}` : 'read'; + const name = relative(file.path, session.cwd); + const room = Math.max(4, width - displayWidth(change) - 1); + out.push(`${file.edits || file.writes ? primary : secondary}${padCells(truncateStart(name, room), room, 0)}${RESET} ${file.edits || file.writes ? secondary : subtle}${change}${RESET}`); + } + if (files.length > 10) line(`+${files.length - 10} more`, subtle); + } + + if (telemetry.compactions.length) { + const last = telemetry.compactions.at(-1)!; + title(`Compacted ${telemetry.compactions.length}×`.replace('×', safe() ? 'x' : '×')); + line(`${last.trigger} · ${formatTokens(last.preTokens)}${last.postTokens ? ` → ${formatTokens(last.postTokens)}` : ''} · ${age(last.at, now)}`.replace('→', safe() ? '->' : '→'), subtle); + } + + const limits = Object.entries(telemetry.rateLimits).filter(([, limit]) => limit.value.status !== 'allowed' || (limit.value.utilization ?? 0) >= 0.7); + if (limits.length) { + title('Usage limits'); + for (const [type, limit] of limits) { + const words = type.replace('_', ' ').replace('five hour', '5-hour').replace('seven day', '7-day'); + const use = limit.value.utilization !== undefined ? ` ${Math.round(limit.value.utilization * 100)}%` : ''; + const reset = limit.value.resetsAt ? ` · resets ${new Date(limit.value.resetsAt).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'})}` : ''; + line(`${words}${use}${reset}${limit.value.from ? ` · via ${displayText(limit.value.from)}` : ''}`, limit.value.status === 'rejected' ? failure : secondary); + } + } + + const usage = telemetry.usage; + if (usage) { + title('Session'); + line(`${usage.turns} turn${usage.turns === 1 ? '' : 's'}${usage.costUsd !== undefined ? ` · ~$${usage.costUsd.toFixed(2)} estimated` : ''}`, subtle); + } + return fit(out, width, height); +} + +function context(telemetry: AgentTelemetry, width: number, now: number, title: (text: string) => void, line: (text: string, style?: string) => void, + c: {primary: string; secondary: string; subtle: string; failure: string}): void { + const meter = contextMeter(telemetry); + title('Context'); + if (!meter) { line('Not reported yet', c.subtle); line('/context asks Claude', c.subtle); return; } + const percent = Math.round(meter.percent); + const label = `${percent}%`; + const cells = Math.max(4, Math.min(20, width - displayWidth(label) - 1)); + line(`${bar(meter.percent, cells)} ${label}`, percent >= 85 ? c.failure : c.primary); + line(`${formatTokens(meter.used)} of ${formatTokens(meter.max)}`, c.secondary); + const snapshot = meter.source === 'provider' ? telemetry.context?.value : undefined; + if (snapshot?.autoCompact && snapshot.autoCompactThreshold) { + const headroom = Math.max(0, snapshot.autoCompactThreshold - snapshot.totalTokens); + line(`${formatTokens(headroom)} before auto-compact`, c.secondary); + } + line(meter.source === 'provider' ? `Claude's count · ${age(meter.at, now)}` : `estimate from the last turn · ${age(meter.at, now)}`, c.subtle); + if (snapshot) { + const used = snapshot.categories.filter(category => category.kind === 'used' && category.tokens > 0).sort((a, b) => b.tokens - a.tokens).slice(0, 6); + for (const category of used) { + const value = formatTokens(category.tokens); + const room = Math.max(4, width - displayWidth(value) - 1); + line(`${padCells(truncateText(displayText(category.name), room), room, 0)} ${value}`, c.subtle); + } + } +} + +function truncateStart(text: string, width: number): string { + if (displayWidth(text) <= width) return text; + const ellipsis = safe() ? '...' : '…'; + let tail = ''; + for (const ch of [...text].reverse()) { if (displayWidth(ch + tail) + displayWidth(ellipsis) > width) break; tail = ch + tail; } + return ellipsis + tail; +} + +function fit(lines: string[], width: number, height: number): string[] { + const rows = lines.slice(0, Math.max(0, height)); + while (rows.length < height) rows.push(''); + return rows.map(row => displayWidth(stripAnsi(row)) > width ? truncateAnsi(row, width) : row); +} diff --git a/src/app/TerminalApp.ts b/src/app/TerminalApp.ts index 1d7a295c..e4294800 100644 --- a/src/app/TerminalApp.ts +++ b/src/app/TerminalApp.ts @@ -158,7 +158,7 @@ import {InProcessSessionClient} from '../session/InProcessSessionClient.js'; import {cursorStyleSequence, TerminalRenderer} from '../terminal/TerminalRenderer.js'; import {KeyDecoder, type Key} from '../terminal/keys.js'; import {promptConfigurationPath} from '../configuration/paths.js'; -import {displayWidth, repeatToWidth, stripAnsi, truncateAnsi, truncateText} from '../util/text.js'; +import {displayWidth, padCells, repeatToWidth, stripAnsi, truncateAnsi, truncateText} from '../util/text.js'; import {isSlashInput, parseSlashCommand, slashCommands, slashSuggestions, suggestionWindow, type ParsedSlashCommand} from '../commands/slashCommands.js'; import {ClipboardUnavailableError, copyFeedback, copyStats, writeClipboard} from '../clipboard/clipboard.js'; import {beginSelection, extendSelection, isRowSelected, selectedText, type TranscriptSelection} from '../output/TranscriptSelection.js'; @@ -314,6 +314,7 @@ import {inspectPlugin, setPluginEnabled} from '../agents/mods/claudePlugins.js'; import {openEffortPicker, openModelPicker} from '../agents/input/pickers.js'; import {applyPickerChoice} from '../agents/input/surface.js'; import {settingsEffort} from '../agents/claudeSettings.js'; +import {renderSidePanel, sidePanelShown, sidePanelWidth} from '../agents/workspace/sidePanel.js'; import {agentBlocks, agentPanelRows, renderAgentPanel, renderAgentView, renderShelf, shelfOrder, type AgentPanelState, type AgentViewMeta, type AgentViewState} from '../agents/sessions/AgentViews.js'; import {harness} from '../agents/harnesses.js'; import {defaultRuntimeDir, socketPathFor} from '../session/runtimeDir.js'; @@ -3979,8 +3980,16 @@ export class TerminalApp { if (session) { // The agent draft uses the shell composer's geometry and the terminal's own caret: the view reports where it is. const layout: {caret?: {row: number; column: number}} = {}; - const rows = framePanel(renderAgentView(session, this.agentView, columns, this.dimensions().rows - (this.agentView.controller ? 1 : 4), Date.now(), this.agentViewMeta(session), - {presentation: this.promptConfiguration.transcriptPresentation, composerPosition: this.promptConfiguration.composerPosition === 'top' ? 'top' : 'bottom', hardwareCaret: true, layout}), columns); + const height = this.dimensions().rows - (this.agentView.controller ? 1 : 4); + // The right panel sits beside the conversation when shown; the conversation keeps the composer and caret. + const panel = session.level === 'managed' && sidePanelShown(this.agentView.panel, columns); + const panelWidth = panel ? sidePanelWidth(columns) : 0; + const mainWidth = columns - (panel ? panelWidth + 3 : 0); + const main = renderAgentView(session, this.agentView, mainWidth, height, Date.now(), this.agentViewMeta(session), + {presentation: this.promptConfiguration.transcriptPresentation, composerPosition: this.promptConfiguration.composerPosition === 'top' ? 'top' : 'bottom', hardwareCaret: true, layout}); + const side = panel ? renderSidePanel(session, panelWidth, main.length, Date.now(), this.agentSettingsEffort.get(session.id)) : []; + const divider = `${foreground(UI_COLORS.subtle)}${getCurrentGlyphMode() === 'safe' ? '|' : '│'}\u001b[0m`; + const rows = framePanel(panel ? main.map((row, index) => `${padCells(row, mainWidth, 0)} ${divider} ${side[index] ?? ''}`) : main, columns); // framePanel puts its rule first; a Top panel later moves that rule to its last row. this.agentCaret = layout.caret && {row: layout.caret.row + 1, column: layout.caret.column}; return rows; @@ -8028,6 +8037,16 @@ export class TerminalApp { if (!this.agents.controllable(target.id)) return {ok: false, reason: `/${kind} needs a running managed Claude target with a live control channel.`}; return kind === 'model' ? openModelPicker(target.telemetry) : openEffortPicker(target.telemetry, this.agentSettingsEffort.get(target.id)); }, + togglePanel: state => { + const columns = this.dimensions().columns; + state.panel = !sidePanelShown(state.panel, columns); + if (state.panel && columns < 72) { state.panel = undefined; return 'The panel needs at least 72 columns; widen the terminal.'; } + return state.panel ? 'Panel shown. /panel hides it.' : 'Panel hidden. /panel shows it.'; + }, + refreshContext: target => {void this.agents.contextUsage(target.id).then(result => { + if (this.agentView?.sessionId === target.id) this.agentView.message = result.ok ? `Context: ${result.value.totalTokens.toLocaleString()} of ${result.value.maxTokens.toLocaleString()} tokens, by Claude's count.` : `Context unavailable: ${result.reason}`; + if (!this.stopped) this.render(); + });}, applyPicker: (target, picker, row) => {void applyPickerChoice(view, picker, row, { setModel: model => this.agents.setModel(target.id, model), setEffort: level => this.agents.setEffort(target.id, level), render: () => { if (!this.stopped) this.render(); }});}, copy: text => {void writeClipboard(text).then(() => {view.message = 'Copied.'; this.render();}).catch(() => {view.message = 'Clipboard unavailable.'; this.render();});}, diff --git a/tests/agentSidePanel.test.ts b/tests/agentSidePanel.test.ts new file mode 100644 index 00000000..852fd4ab --- /dev/null +++ b/tests/agentSidePanel.test.ts @@ -0,0 +1,100 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {renderSidePanel, sidePanelShown, sidePanelWidth} from '../src/agents/workspace/sidePanel.js'; +import {applyTelemetry, emptyTelemetry} from '../src/agents/telemetry.js'; +import {parseCatalog, parseContextUsage} from '../src/agents/sessions/claudeStream.js'; +import {AgentInputController} from '../src/agents/input/controller.js'; +import {handleAgentInput, type AgentSurfaceHost} from '../src/agents/input/surface.js'; +import type {AgentSession} from '../src/agents/sessions/model.js'; +import type {AgentViewState} from '../src/agents/sessions/AgentViews.js'; +import {AgentTranscript} from '../src/agents/transcript/model.js'; +import {setIconStyle} from '../src/ui/glyphs.js'; +import {displayWidth, stripAnsi} from '../src/util/text.js'; +import type {Key} from '../src/terminal/keys.js'; + +const NOW = 1_000_000; +function session(): AgentSession { + const t = emptyTelemetry(); + applyTelemetry(t, {kind: 'catalog', catalog: parseCatalog({commands: [], agents: [], models: [{value: 'sonnet', resolvedModel: 'claude-sonnet-5-5', displayName: 'Sonnet 5.5', supportsEffort: true, supportedEffortLevels: ['low', 'medium', 'high']}]}, 1)!}); + applyTelemetry(t, {kind: 'runtime', runtime: {model: 'claude-sonnet-5-5', permissionMode: 'acceptEdits', tools: [], mcpServers: [], slashCommands: [], skills: [], plugins: [], capabilities: [], at: NOW - 5000}}); + applyTelemetry(t, {kind: 'usage', at: NOW - 4000, usage: {perModel: {'claude-sonnet-5-5': {input: 1, output: 1, cacheRead: 1, cacheWrite: 1, contextWindow: 1_000_000}}, + lastTurn: {input: 6, output: 271, cacheRead: 180_000, cacheWrite: 3146, model: 'claude-sonnet-5-5'}, turns: 3, durationMs: 1, apiMs: 1, costUsd: 0.4123}}); + applyTelemetry(t, {kind: 'todos', at: NOW, todos: [{content: 'Read the parser', status: 'completed'}, {content: 'Fix routing', status: 'in_progress', activeForm: 'Fixing routing'}, {content: 'Add tests', status: 'pending'}]}); + applyTelemetry(t, {kind: 'file', path: '/w/src/commands/slashCommands.ts', action: 'read', at: NOW - 3000}); + applyTelemetry(t, {kind: 'file', path: '/w/src/app/TerminalApp.ts', action: 'edit', added: 12, removed: 3, at: NOW - 2000}); + applyTelemetry(t, {kind: 'task', at: NOW - 9000, task: {id: 'k', description: 'Find callers of isSlashInput', subagent: 'Explore', status: 'running', tokens: 5400}}); + applyTelemetry(t, {kind: 'rateLimit', type: 'five_hour', at: NOW, limit: {status: 'allowed_warning', utilization: 0.82}}); + applyTelemetry(t, {kind: 'rateLimit', type: 'seven_day', at: NOW, limit: {status: 'allowed', utilization: 0.2}}); + return {id: 'a', harness: 'claude', level: 'managed', title: 'Routing', cwd: '/w', startedAt: 0, state: 'working', events: [], attention: false, updatedAt: 0, transcript: new AgentTranscript(), telemetry: t}; +} + +test('panel: context with its source, model, plan, subagents, files with changes, close limits and estimated cost', () => { + const text = renderSidePanel(session(), 40, 60, NOW).map(row => stripAnsi(row)).join('\n'); + assert.match(text, /^Context\n.+ 18%\n183k of 1M\nestimate from the last turn · now$/mu, 'an estimate says it is one, with its age'); + assert.match(text, /Model\nSonnet 5\.5\nEffort: model default\nPermissions acceptEdits/u); + assert.match(text, /Plan 1\/3\n✓ Read the parser\n▸ Fixing routing\n· Add tests/u); + assert.match(text, /Subagents and tasks\nExplore · Find callers of isSlashInput\n {2}9s · 5\.4k tok/u); + assert.match(text, /Files 1 changed · 2 touched\nsrc\/app\/TerminalApp\.ts +\+12 -3\nsrc\/commands\/slashCommands\.ts +read/u); + assert.match(text, /Usage limits\n5-hour 82%/u); + assert.doesNotMatch(text, /7-day/u, 'a limit far from its cap stays out of the way'); + assert.match(text, /3 turns · ~\$0\.41 estimated/u); +}); + +test('panel: Claude\'s own breakdown replaces the estimate, with headroom to auto-compact and the largest categories', () => { + const s = session(); + applyTelemetry(s.telemetry!, {kind: 'context', at: NOW, snapshot: parseContextUsage({categories: [{name: 'System prompt', tokens: 2356, kind: 'used'}, {name: 'Messages', tokens: 120_000, kind: 'used'}, {name: 'Free space', tokens: 800_000, kind: 'free'}], + totalTokens: 122_356, maxTokens: 1_000_000, percentage: 12, autoCompactThreshold: 967_000, isAutoCompactEnabled: true, memoryFiles: [], mcpTools: [], agents: []}, 'summary')!}); + const text = renderSidePanel(s, 40, 60, NOW).map(row => stripAnsi(row)).join('\n'); + assert.match(text, /12%\n122k of 1M\n845k before auto-compact\nClaude's count · now\nMessages +120k\nSystem prompt +2\.4k/u); + assert.doesNotMatch(text, /Free space/u); +}); + +test('panel: nothing reported means words, never 0%; every width fits; Safe glyphs and NO_COLOR stay readable', () => { + const bare: AgentSession = {...session(), telemetry: emptyTelemetry()}; + const empty = renderSidePanel(bare, 32, 10, NOW).map(row => stripAnsi(row)); + assert.deepEqual(empty.slice(0, 3), ['Context', 'Not reported yet', '/context asks Claude']); + assert.ok(!empty.join('\n').includes('0%')); + for (const width of [30, 36, 46]) for (const height of [6, 20, 60]) { + const rows = renderSidePanel(session(), width, height, NOW); + assert.equal(rows.length, height); + assert.ok(rows.every(row => displayWidth(stripAnsi(row)) <= width), `${width}x${height}`); + } + const before = process.env.NO_COLOR; + setIconStyle('safe'); + process.env.NO_COLOR = '1'; + try { + const rows = renderSidePanel(session(), 40, 60, NOW).join('\n'); + assert.doesNotMatch(rows, /\u001b\[(?:38|48);/u); + const plain = stripAnsi(rows); + assert.match(plain, /#+-+ 18%/u); + assert.match(plain, /\[x\] Read the parser\n\[>\] Fixing routing\n\[ \] Add tests/u); + assert.doesNotMatch(plain.replace(/ · /gu, ' - '), /[█░✓▸…→×]/u); + } finally { setIconStyle('nerd'); if (before === undefined) delete process.env.NO_COLOR; else process.env.NO_COLOR = before; } +}); + +test('panel placement: on by default from 120 columns, the person\'s choice wins, never below 72; width stays bounded', () => { + assert.equal(sidePanelShown(undefined, 119), false); + assert.equal(sidePanelShown(undefined, 120), true); + assert.equal(sidePanelShown(false, 200), false); + assert.equal(sidePanelShown(true, 90), true); + assert.equal(sidePanelShown(true, 60), false); + assert.deepEqual([80, 120, 200, 400].map(sidePanelWidth), [30, 36, 46, 46]); +}); + +test('/panel and /context are NMSh commands in the agent composer; nothing is sent to Claude', () => { + const s = session(); + const v: AgentViewState = {sessionId: 'a', input: '', expanded: new Set(), scroll: 0, controller: new AgentInputController()}; + const sent: string[] = []; + let refreshed = 0; + const host: AgentSurfaceHost = {shellEmpty: true, send: (_id, text) => { sent.push(text); return true; }, answer: () => false, cancel: () => {}, copy: () => {}, copyReply: () => {}, + togglePanel: view => { view.panel = !view.panel; return view.panel ? 'Panel shown. /panel hides it.' : 'Panel hidden. /panel shows it.'; }, refreshContext: () => { refreshed++; }}; + const type = (text: string) => { for (const ch of text) handleAgentInput(v, s, {kind: 'text', value: ch} as Key, host); handleAgentInput(v, s, {kind: 'enter'} as Key, host); }; + type('/panel'); + assert.equal(v.panel, true); + assert.equal(v.message, 'Panel shown. /panel hides it.'); + type('/context'); + assert.equal(refreshed, 1); + assert.equal(v.panel, true, '/context shows the panel the breakdown appears in'); + type('/contexts please'); + assert.deepEqual(sent, ['/contexts please'], 'anything else still goes to Claude'); +}); From 2dad06cd03cd9b2c95e7b772c2f0d37756670228 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:50:56 +0530 Subject: [PATCH 010/249] Design: agent workspace controls, Claude mods and the context panel Mechanisms chosen for each change, what is operational versus a representation of provider facts versus unsupported, the research conclusions that shaped the panel, and the next steps (left sidebar, per-turn diffs, MCP status, prompt navigation). --- docs/design/agent-workspace-controls.md | 85 +++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 docs/design/agent-workspace-controls.md diff --git a/docs/design/agent-workspace-controls.md b/docs/design/agent-workspace-controls.md new file mode 100644 index 00000000..7f283120 --- /dev/null +++ b/docs/design/agent-workspace-controls.md @@ -0,0 +1,85 @@ +# Agent workspace: native controls, Claude mods and the context panel + +Continues [Managed Targets, agent UI, and unified mods](claude-managed-targets-agent-ui.md). Verified against +Claude Code 2.1.295 (2026-10-08) and its documentation on 2026-10-09. + +## Principles + +- **NMSh owns its interface; Claude owns its state.** Pickers, lists, confirmation and feedback are NMSh's. Every + change goes through a mechanism Claude documents: control requests on the managed stream-json channel, or Claude's + own `claude plugin …` commands. NMSh never edits Claude's settings files and never types into Claude's text menus. +- **Nothing is claimed without evidence.** Each fact carries its provenance (`provider`, `requested`, `observed`, + `estimate`) and age. A change is "requested" until Claude reports it; an estimate says it is one; an unreported value + is said in words, never shown as 0% or a guessed level. +- **A command beginning with `/` is not automatically NMSh's.** `isSlashInput()` is the single rule: a first word with + a `/` after the leading one is a path and belongs to the shell. In a managed agent composer only exact NMSh forms + (`/model`, `/effort`, `/panel`, `/context`, `/approval`, `/copy`) are intercepted; a leading space sends text verbatim. + +## Mechanisms + +| Need | Mechanism | Takes effect | +|---|---|---| +| Model | control request `set_model` | from Claude's next model call, mid-turn included; confirmed when `system/init` reports it | +| Effort | `apply_flag_settings {effortLevel}` (`null` = model default) | next turn, this session only | +| Launch effort | `--effort` flag from a launch profile (only when `--help` documents it) | the whole session | +| Context breakdown | `get_context_usage` (`summary`) | on request (`/context`) | +| Plugin inventory | `claude plugin list --json` per launch identity (`CLAUDE_CONFIG_DIR`) | read | +| Plugin contents | manifest files read as bounded JSON: `.claude-plugin/plugin.json`, `hooks/hooks.json`, component folders | read; nothing imported or run | +| Enable / disable | `claude plugin enable\|disable --scope --json` | settings; then `reload_plugins` to running managed sessions of that account | +| Inspect | `claude plugin details `, `claude plugin validate ` (static) | read | + +Claude's `plugin enable` accepts an id that is not installed and writes it into settings. NMSh only sends ids from the +current listing, and reads the listing again afterwards as evidence. + +## What is operational, what is representation, what is unsupported + +**Operational (real provider effect, verified by tests against the documented wire format and by the 2.1.295 probe):** + +- `/model` picker and `/model `: `set_model`, acknowledgement awaited, refusal kept beside the rows. +- `/effort` picker and `/effort `: `apply_flag_settings`, only the running model's `supportedEffortLevels`. +- `/context`: Claude's own breakdown in the right panel (categories, headroom to auto-compact). +- Plugin enable/disable at the installed scope and account, with listing re-read and live `reload_plugins`. +- Absolute executable paths in the shell composer, verified end to end on zsh, Bash and Fish. + +**Representation of provider facts (display only, with provenance):** + +- Header model and effort; right panel context meter (estimate between breakdowns), plan (TodoWrite), files read and + edited with line counts, subagents and tasks, compactions, usage limits near their cap, estimated cost. +- Plugin type, components, dependencies, scope conflicts, project overrides, "Loaded in" (from a target's own report). +- What a plugin does in a managed session: per Claude's docs, under `claude -p` a mod's hooks run but nothing it draws + appears (skins, filetree panes, terminal-browser, replay-theater's viewer); skills, agents, commands, settings hooks, + MCP and LSP servers load as usual. + +**Unsupported, said in the UI rather than faked:** + +- The effort level in effect is never reported by headless Claude; NMSh shows the acknowledged request, launch flag or + settings value, labelled as such. +- Personal skills (`skills/` folder) and settings hooks have no Claude command to toggle; they are listed with the reason. +- A plugin decided by the project's `.claude/settings(.local).json` is not toggled at user scope; the override is named. +- Mod panes, bands and restyled rows: not hosted by NMSh (no NMSh sandbox exists or is implied). +- Claude's own terminal sessions see a plugin change after `/reload-plugins` or a restart. + +## Research conclusions applied + +Two research passes (coding-agent interfaces and context tooling; workspace navigation and TUI craft) are summarized in +the PR. The decisions they drove: + +- **Two tiers of context.** A quiet pressure snapshot (bar, used of window, headroom, source and age) always in the + panel; the attribution report only on request. Never `null → 0%` (Claude HUD #330/#576); name the source because + Claude's own figures differ by denominator (CC #63015). +- **Model and effort pickers converge on one shape** (Claude Code, Codex): catalog rows with current/default marks, + per-model effort levels, explicit timing. NMSh adds provenance and keeps Enter as the only action. +- **Show the target of every file activity** (CC #21151): the panel lists paths with `+a -r` or `read`. +- **Plan only when the provider emits one**: task tools are off by default on 5.5-generation models. +- **Panels give way before the conversation does.** Right panel by default from 120 columns, on request from 72, + bounded to 30–46 columns; below that the conversation is full width. Breakpoints follow Herdr and lazygit practice. +- **State before decoration.** No transcript parsing or network on the render path (status-line tools' CPU and 429 + storms); everything comes from events NMSh already receives. + +## Next steps (not in this change) + +- Left workspace sidebar (projects, shell sessions, managed targets, worktrees, PRs) with Herdr's state vocabulary + (blocked / working / done-unseen / idle / unknown, worst-state roll-up) and selection ≠ preview ≠ activation. +- Per-turn diff source in the right panel from the provider's `structuredPatch` hunks already retained. +- MCP server status and session toggles in the panel (`mcp_status`, `mcp_toggle` are implemented in the adapter). +- Prompt-to-prompt navigation (`{` / `}`) in the agent transcript, matching Claude Code's transcript view. From 35dfc0fa6d5e177d34ccb5a0eee6531ad0adb282 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:00:31 +0530 Subject: [PATCH 011/249] Probe: live /model and /effort through NMSh's managed session against real Claude Runs the picker logic and the documented control requests against a logged-in Claude Code in safe mode (no plugins, hooks or mods run, no session persistence), and proves the account's settings.json is unchanged. Verified with 2.1.295: the catalog opens the picker, set_model reads as requested until the next turn reports the model, effort is acknowledged (never claimed as confirmed), and an unknown model is refused in Claude's own words. --- scripts/probes/managed-pickers-live.ts | 102 +++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 scripts/probes/managed-pickers-live.ts diff --git a/scripts/probes/managed-pickers-live.ts b/scripts/probes/managed-pickers-live.ts new file mode 100644 index 00000000..0201f397 --- /dev/null +++ b/scripts/probes/managed-pickers-live.ts @@ -0,0 +1,102 @@ +/** + * Live validation of NMSh's /model and /effort pickers against a real Claude Code, through NMSh's own managed-session + * code (AgentSessions → ClaudeSession → the documented control requests) and the picker logic the UI uses. + * + * Isolation: `claude` resolves to a wrapper that adds --safe-mode (no plugins, hooks or mods of the account run), + * --setting-sources '' and --no-session-persistence; the work directory is a fresh temp folder. Only session-scoped + * controls are exercised (set_model, apply_flag_settings), so no settings are written; the account's settings.json is + * hashed before and after to prove it. Prints shapes and outcomes only, never account identity values. + * + * NMSH_PROBE_CONFIG_DIR=~/.claude-account2 node --import=tsx scripts/probes/managed-pickers-live.ts + */ +import {spawnSync} from 'node:child_process'; +import {createHash} from 'node:crypto'; +import {chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync} from 'node:fs'; +import {homedir, tmpdir} from 'node:os'; +import {join} from 'node:path'; +import {AgentSessions} from '../../src/agents/sessions/manager.js'; +import {openEffortPicker, openModelPicker} from '../../src/agents/input/pickers.js'; +import {applyPickerChoice} from '../../src/agents/input/surface.js'; +import {AgentInputController} from '../../src/agents/input/controller.js'; +import {currentModel, effortStatus} from '../../src/agents/telemetry.js'; +import type {AgentViewState} from '../../src/agents/sessions/AgentViews.js'; + +const real = process.env.NMSH_PROBE_CLAUDE ?? join(homedir(), '.local/bin/claude'); +const configDir = process.env.NMSH_PROBE_CONFIG_DIR; +if (!configDir || !existsSync(join(configDir, 'settings.json'))) throw new Error('Set NMSH_PROBE_CONFIG_DIR to a logged-in Claude config directory'); +const hash = () => createHash('sha256').update(readFileSync(join(configDir, 'settings.json'))).digest('hex'); +const before = hash(); +const root = mkdtempSync(join(tmpdir(), 'nmsh-pickers-')); +const wrapper = join(root, 'claude'); +writeFileSync(wrapper, `#!/bin/sh\nexec ${JSON.stringify(real)} "$@" --safe-mode --setting-sources '' --no-session-persistence --max-budget-usd 0.5\n`); +chmodSync(wrapper, 0o755); +const help = spawnSync(real, ['--help'], {encoding: 'utf8', timeout: 10000}).stdout; +const note = (step: string, value: unknown) => console.log(JSON.stringify({step, ...value as object})); +const until = async (what: string, check: () => boolean, ms = 120000) => { + const end = Date.now() + ms; + while (!check()) { if (Date.now() > end) throw new Error(`timed out: ${what}`); await new Promise(resolve => setTimeout(resolve, 100)); } +}; + +const sessions = new AgentSessions({resolve: name => name === 'claude' ? wrapper : undefined, scan: async () => [], now: () => Date.now(), claudeHelp: help}); +let failures = 0; +const check = (name: string, ok: boolean, detail: unknown = {}) => { if (!ok) failures++; note(name, {ok, detail}); }; +try { + const launched = sessions.launch('claude', root, {profile: {name: 'probe', harness: 'claude', configDir, model: 'haiku', effort: 'low'}}); + if (!launched.ok) throw new Error(launched.reason); + const session = launched.session; + await until('catalog', () => Boolean(session.telemetry?.catalog)); + const view: AgentViewState = {sessionId: session.id, input: '', expanded: new Set(), scroll: 0, controller: new AgentInputController()}; + const ops = {setModel: (model: string | undefined) => sessions.setModel(session.id, model), setEffort: (level: string | null) => sessions.setEffort(session.id, level), render: () => {}}; + + const catalog = openModelPicker(session.telemetry); + check('model picker opens from the real catalog', catalog.ok, catalog.ok ? {rows: catalog.picker.rows.map(row => `${row.value}${row.current ? '*' : ''}`), status: catalog.picker.status} : catalog); + check('launch effort is labelled as a launch flag', effortStatus(session.telemetry).state === 'launch', effortStatus(session.telemetry)); + + const turn = async (text: string) => { + const settled = session.events.filter(event => event.kind === 'settled').length; + sessions.send(session.id, text); + await until('turn', () => session.events.filter(event => event.kind === 'settled').length > settled); + }; + await turn('Reply with exactly one word: alpha'); + check('first turn reports the launch model', /haiku/u.test(session.telemetry?.runtime?.model ?? ''), {model: session.telemetry?.runtime?.model}); + + // /model → Sonnet through the picker, as Enter does. + const picker = openModelPicker(session.telemetry); + const sonnet = picker.ok ? picker.picker.rows.find(row => row.value === 'sonnet') : undefined; + check('the catalog offers sonnet', Boolean(sonnet)); + if (picker.ok && sonnet) { + view.controller!.picker = picker.picker; view.controller!.owner = 'PICKER'; + const applied = await applyPickerChoice(view, picker.picker, sonnet, ops); + check('set_model acknowledged; the picker closes', applied && view.controller!.owner === 'AGENT_MESSAGE', {message: view.message}); + check('the model reads as requested until Claude reports it', currentModel(session.telemetry!)?.source === 'requested', currentModel(session.telemetry!)); + } + + // /effort → high for the requested model's own levels. + const effort = openEffortPicker(session.telemetry); + check('effort picker lists the requested model\'s levels', effort.ok, effort.ok ? {rows: effort.picker.rows.map(row => `${row.value ?? 'default'}${row.current ? '*' : ''}`), status: effort.picker.status} : effort); + const high = effort.ok ? effort.picker.rows.find(row => row.value === 'high') : undefined; + if (effort.ok && high) { + const applied = await applyPickerChoice(view, effort.picker, high, ops); + check('apply_flag_settings acknowledged', applied, {message: view.message}); + check('effort reads as acknowledged, not confirmed', effortStatus(session.telemetry).state === 'acknowledged', effortStatus(session.telemetry)); + } + + await turn('Reply with exactly one word: beta'); + check('the next turn reports the new model', /sonnet/u.test(session.telemetry?.runtime?.model ?? ''), {model: session.telemetry?.runtime?.model}); + check('the model is now reported by Claude, not requested', currentModel(session.telemetry!)?.source === 'provider', currentModel(session.telemetry!)); + const reopened = openModelPicker(session.telemetry); + check('the picker marks the new model current', reopened.ok && reopened.picker.rows.find(row => row.current)?.value === 'sonnet', reopened.ok ? reopened.picker.status : reopened); + + // A refusal stays visible and claims nothing. + const refused = await sessions.setModel(session.id, 'not-a-real-model-xyz'); + note('set_model with an unknown model', {result: refused}); + const replies = session.events.filter(event => event.kind === 'assistant').map(event => (event as {text: string}).text.trim().slice(0, 20)); + note('replies', {replies, controls: session.events.filter(event => event.kind === 'control').map(event => (event as {label: string}).label)}); +} finally { + sessions.dispose(); + await new Promise(resolve => setTimeout(resolve, 1500)); + check('account settings.json unchanged', hash() === before); + rmSync(root, {recursive: true, force: true}); + console.log(JSON.stringify({failures})); + process.exit(failures ? 1 : 0); +} From 1fd0eb55251cc713ac0ae79c5cc02249acfaf8a7 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:47:45 +0530 Subject: [PATCH 012/249] Probe: live plugin toggle inside an NMSh-managed session, in a disposable configuration Installs a code-free probe plugin from a local marketplace into a disposable, separately logged-in Claude configuration, starts a managed session that loads it, then disables and re-enables it through NMSh's own path (listing, setPluginEnabled, reload_plugins) and checks Claude's own reports: plugin list, offered commands, load errors. Refuses any non-temp or account-looking directory and hashes the real accounts' settings before and after. --- scripts/probes/managed-plugins-live.ts | 97 ++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 scripts/probes/managed-plugins-live.ts diff --git a/scripts/probes/managed-plugins-live.ts b/scripts/probes/managed-plugins-live.ts new file mode 100644 index 00000000..b2cb904a --- /dev/null +++ b/scripts/probes/managed-plugins-live.ts @@ -0,0 +1,97 @@ +/** + * Live validation of NMSh's Claude plugin toggle inside an NMSh-managed Claude session, in a DISPOSABLE configuration: + * install a probe plugin from a local marketplace, start a managed session that loads it, then disable and re-enable + * it through NMSh's own code (Claude's listing → setPluginEnabled → reload_plugins) and read Claude's own reports to + * see it unload and load. Refuses real account directories; hashes the real accounts' settings before and after. + * + * CLAUDE_CONFIG_DIR=/tmp/nmsh-cc-probe claude auth login # once, by the person + * NMSH_PROBE_CONFIG_DIR=/tmp/nmsh-cc-probe NMSH_PROBE_MARKET=/tmp/nmsh-cc-market node --import=tsx scripts/probes/managed-plugins-live.ts + */ +import {spawnSync} from 'node:child_process'; +import {createHash} from 'node:crypto'; +import {chmodSync, existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync} from 'node:fs'; +import {homedir, tmpdir} from 'node:os'; +import {join} from 'node:path'; +import {AgentSessions} from '../../src/agents/sessions/manager.js'; +import {claudePluginEntries, listClaudePlugins, setPluginEnabled} from '../../src/agents/mods/claudePlugins.js'; +import {withLoadedIn} from '../../src/agents/mods/inventory.js'; + +const real = process.env.NMSH_PROBE_CLAUDE ?? join(homedir(), '.local/bin/claude'); +const configDir = process.env.NMSH_PROBE_CONFIG_DIR ? realpathSync(process.env.NMSH_PROBE_CONFIG_DIR) : ''; +const market = process.env.NMSH_PROBE_MARKET ?? ''; +if (!configDir || !market) throw new Error('Set NMSH_PROBE_CONFIG_DIR (disposable, logged in) and NMSH_PROBE_MARKET'); +// Never a real account: the disposable directory must live in a temp folder. +if (!/^\/(?:private\/)?tmp\//u.test(configDir) || /\.claude(?:-account\d+)?$/u.test(configDir)) throw new Error(`Refusing ${configDir}: not a disposable temp configuration`); +const realSettings = ['.claude', '.claude-account1', '.claude-account2'].map(dir => join(homedir(), dir, 'settings.json')).filter(existsSync); +const hashes = () => realSettings.map(path => createHash('sha256').update(readFileSync(path)).digest('hex')).join(','); +const before = hashes(); +const env = {...process.env, CLAUDE_CONFIG_DIR: configDir}; +const claude = (args: string[]) => spawnSync(real, args, {env, encoding: 'utf8', timeout: 60000}); +const auth = JSON.parse(claude(['auth', 'status']).stdout || '{}') as {loggedIn?: boolean}; +if (!auth.loggedIn) throw new Error('The disposable configuration is not logged in'); + +const note = (step: string, value: unknown) => console.log(JSON.stringify({step, ...value as object})); +let failures = 0; +const check = (name: string, ok: boolean, detail: unknown = {}) => { if (!ok) failures++; note(name, {ok, detail}); }; +const until = async (what: string, test: () => boolean, ms = 120000) => { + const end = Date.now() + ms; + while (!test()) { if (Date.now() > end) throw new Error(`timed out: ${what}`); await new Promise(resolve => setTimeout(resolve, 100)); } +}; + +const work = mkdtempSync(join(tmpdir(), 'nmsh-plugins-work-')); +const wrapper = join(work, 'claude'); +writeFileSync(wrapper, `#!/bin/sh\nexec ${JSON.stringify(real)} "$@" --no-session-persistence --max-budget-usd 0.5\n`); +chmodSync(wrapper, 0o755); +const sessions = new AgentSessions({resolve: name => name === 'claude' ? wrapper : undefined, scan: async () => [], now: () => Date.now(), claudeHelp: claude(['--help']).stdout}); +try { + // Setup in the disposable configuration only (Claude's own commands). + note('marketplace add', {status: claude(['plugin', 'marketplace', 'add', market]).status}); + note('install', {status: claude(['plugin', 'install', 'nmsh-probe@nmsh-probe-market', '--scope', 'user']).status}); + const entry = async () => (await claudePluginEntries(await listClaudePlugins(real, work, configDir), work, {name: 'probe', configDir})).find(item => item.id === 'nmsh-probe@nmsh-probe-market'); + const installed = await entry(); + check('the probe plugin is listed, enabled, with a switch', installed?.enabled === 'yes' && installed.toggle?.supported === true, installed && {type: installed.nativeType, components: installed.components}); + + const launched = sessions.launch('claude', work, {profile: {name: 'probe', harness: 'claude', configDir, model: 'haiku'}}); + if (!launched.ok) throw new Error(launched.reason); + const session = launched.session; + const turn = async (text: string) => { + const settled = session.events.filter(event => event.kind === 'settled').length; + sessions.send(session.id, text); + await until('turn', () => session.events.filter(event => event.kind === 'settled').length > settled); + }; + await turn('Reply with exactly one word: ready'); + const loaded = () => (session.telemetry?.runtime?.plugins ?? []).map(plugin => plugin.name); + check('Claude reports the plugin loaded in the managed session', loaded().includes('nmsh-probe'), {plugins: loaded()}); + check('"Loaded in" names the session', withLoadedIn([installed!], sessions.sessions)[0]!.loadedIn?.length === 1); + const commandBefore = (session.telemetry?.runtime?.slashCommands ?? []).filter(name => name.includes('nmsh-probe')); + check('its command is offered', commandBefore.length > 0, {commands: commandBefore}); + + // Disable through NMSh's toggle path. + const ids = new Set(['nmsh-probe@nmsh-probe-market']); + const off = await setPluginEnabled(real, installed!, false, ids); + check('claude plugin disable succeeded', off.ok, off); + check('the listing confirms it disabled', (await entry())?.enabled === 'no'); + const reloadOff = await sessions.reloadPlugins(session.id); + check('reload_plugins: Claude no longer loads it', reloadOff.ok && !reloadOff.value.plugins.includes('nmsh-probe'), reloadOff); + await turn('Reply with exactly one word: after'); + check('the next turn no longer offers its command', !(session.telemetry?.runtime?.slashCommands ?? []).some(name => name.includes('nmsh-probe')), {commands: session.telemetry?.runtime?.slashCommands?.filter(name => name.includes('probe'))}); + + // Enable again. + const on = await setPluginEnabled(real, (await entry())!, true, ids); + check('claude plugin enable succeeded', on.ok, on); + const reloadOn = await sessions.reloadPlugins(session.id); + check('reload_plugins: Claude loads it again, with no load errors', reloadOn.ok && reloadOn.value.plugins.includes('nmsh-probe') && reloadOn.value.errors === 0, reloadOn); + + // An id that is not installed is never sent (Claude's own command would write it into settings). + const ghost = await setPluginEnabled(real, {...installed!, claude: {...installed!.claude!, pluginId: 'ghost@nowhere'}}, true, ids); + check('an uninstalled id is refused before any command runs', !ghost.ok, ghost); + const settings = JSON.parse(readFileSync(join(configDir, 'settings.json'), 'utf8')) as {enabledPlugins?: Record}; + check('the disposable settings never gained the ghost id', !('ghost@nowhere' in (settings.enabledPlugins ?? {})), settings.enabledPlugins); +} finally { + sessions.dispose(); + await new Promise(resolve => setTimeout(resolve, 1500)); + check('real account settings unchanged', hashes() === before, {checked: realSettings.length}); + rmSync(work, {recursive: true, force: true}); + console.log(JSON.stringify({failures})); + process.exit(failures ? 1 : 0); +} From 68bb3d428e2c8a1bf5f1d938cee62ba585a02920 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:07:34 +0530 Subject: [PATCH 013/249] Probe guard: only /tmp/nmsh-cc-probe, never a real Claude configuration through any path The plugin probe now accepts only /tmp/nmsh-cc-probe, resolved through symlinks, and refuses a directory that equals, contains or lies inside ~/.claude, ~/.claude-account1, ~/.claude-account2 or an inherited CLAUDE_CONFIG_DIR. Checked before any claude command runs, since even auth status writes into a fresh directory. --- scripts/probes/managed-plugins-live.ts | 27 +++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/scripts/probes/managed-plugins-live.ts b/scripts/probes/managed-plugins-live.ts index b2cb904a..4ee6537b 100644 --- a/scripts/probes/managed-plugins-live.ts +++ b/scripts/probes/managed-plugins-live.ts @@ -11,17 +11,34 @@ import {spawnSync} from 'node:child_process'; import {createHash} from 'node:crypto'; import {chmodSync, existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync} from 'node:fs'; import {homedir, tmpdir} from 'node:os'; -import {join} from 'node:path'; +import {join, resolve} from 'node:path'; import {AgentSessions} from '../../src/agents/sessions/manager.js'; import {claudePluginEntries, listClaudePlugins, setPluginEnabled} from '../../src/agents/mods/claudePlugins.js'; import {withLoadedIn} from '../../src/agents/mods/inventory.js'; const real = process.env.NMSH_PROBE_CLAUDE ?? join(homedir(), '.local/bin/claude'); -const configDir = process.env.NMSH_PROBE_CONFIG_DIR ? realpathSync(process.env.NMSH_PROBE_CONFIG_DIR) : ''; const market = process.env.NMSH_PROBE_MARKET ?? ''; -if (!configDir || !market) throw new Error('Set NMSH_PROBE_CONFIG_DIR (disposable, logged in) and NMSH_PROBE_MARKET'); -// Never a real account: the disposable directory must live in a temp folder. -if (!/^\/(?:private\/)?tmp\//u.test(configDir) || /\.claude(?:-account\d+)?$/u.test(configDir)) throw new Error(`Refusing ${configDir}: not a disposable temp configuration`); + +/** The only configuration this probe may use or change. */ +const DISPOSABLE = '/tmp/nmsh-cc-probe'; +/** Real launch identities, resolved through symlinks; the probe never touches them. */ +const REAL_IDENTITIES = [join(homedir(), '.claude'), join(homedir(), '.claude-account1'), join(homedir(), '.claude-account2'), ...(process.env.CLAUDE_CONFIG_DIR ? [process.env.CLAUDE_CONFIG_DIR] : [])]; +const resolved = (path: string) => { try { return realpathSync(path); } catch { return resolve(path); } }; +const within = (child: string, parent: string) => child === parent || child.startsWith(`${parent}/`); + +/** Every check runs before any claude command (even auth status creates files in a fresh directory). */ +export function disposableConfig(requested: string | undefined): string { + if (!requested) throw new Error(`Set NMSH_PROBE_CONFIG_DIR=${DISPOSABLE}`); + const path = resolved(requested); + if (path !== resolved(DISPOSABLE)) throw new Error(`Refusing ${requested} (${path}): only ${DISPOSABLE} may be used`); + for (const identity of REAL_IDENTITIES) { + const real = resolved(identity); + if (within(path, real) || within(real, path)) throw new Error(`Refusing ${requested}: it resolves to or overlaps the real Claude configuration ${identity}`); + } + return path; +} +const configDir = disposableConfig(process.env.NMSH_PROBE_CONFIG_DIR); +if (!market) throw new Error('Set NMSH_PROBE_MARKET'); const realSettings = ['.claude', '.claude-account1', '.claude-account2'].map(dir => join(homedir(), dir, 'settings.json')).filter(existsSync); const hashes = () => realSettings.map(path => createHash('sha256').update(readFileSync(path)).digest('hex')).join(','); const before = hashes(); From 8979ca6e8e8e7637950a443ffe8f855e15343813 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Sat, 10 Oct 2026 09:36:52 +0530 Subject: [PATCH 014/249] Probes: refuse every ~/.claude-* identity without naming anyone's accounts The plugin probe's guard and settings hashes listed two personal account directories by name; it now discovers the default ~/.claude, every ~/.claude-* sibling and an inherited CLAUDE_CONFIG_DIR, so it refuses any real identity on any machine. The picker probe's usage line no longer points at a real account. --- scripts/probes/managed-pickers-live.ts | 2 +- scripts/probes/managed-plugins-live.ts | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/scripts/probes/managed-pickers-live.ts b/scripts/probes/managed-pickers-live.ts index 0201f397..e8e1db32 100644 --- a/scripts/probes/managed-pickers-live.ts +++ b/scripts/probes/managed-pickers-live.ts @@ -7,7 +7,7 @@ * controls are exercised (set_model, apply_flag_settings), so no settings are written; the account's settings.json is * hashed before and after to prove it. Prints shapes and outcomes only, never account identity values. * - * NMSH_PROBE_CONFIG_DIR=~/.claude-account2 node --import=tsx scripts/probes/managed-pickers-live.ts + * NMSH_PROBE_CONFIG_DIR= node --import=tsx scripts/probes/managed-pickers-live.ts */ import {spawnSync} from 'node:child_process'; import {createHash} from 'node:crypto'; diff --git a/scripts/probes/managed-plugins-live.ts b/scripts/probes/managed-plugins-live.ts index 4ee6537b..cd204961 100644 --- a/scripts/probes/managed-plugins-live.ts +++ b/scripts/probes/managed-plugins-live.ts @@ -9,7 +9,7 @@ */ import {spawnSync} from 'node:child_process'; import {createHash} from 'node:crypto'; -import {chmodSync, existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync} from 'node:fs'; +import {chmodSync, existsSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync} from 'node:fs'; import {homedir, tmpdir} from 'node:os'; import {join, resolve} from 'node:path'; import {AgentSessions} from '../../src/agents/sessions/manager.js'; @@ -21,8 +21,15 @@ const market = process.env.NMSH_PROBE_MARKET ?? ''; /** The only configuration this probe may use or change. */ const DISPOSABLE = '/tmp/nmsh-cc-probe'; -/** Real launch identities, resolved through symlinks; the probe never touches them. */ -const REAL_IDENTITIES = [join(homedir(), '.claude'), join(homedir(), '.claude-account1'), join(homedir(), '.claude-account2'), ...(process.env.CLAUDE_CONFIG_DIR ? [process.env.CLAUDE_CONFIG_DIR] : [])]; +/** + * Real launch identities, resolved through symlinks; the probe never touches them: the default `~/.claude`, every + * `~/.claude-*` sibling (how separate accounts are usually laid out), and an inherited CLAUDE_CONFIG_DIR. + */ +const REAL_IDENTITIES = [ + join(homedir(), '.claude'), + ...readdirSync(homedir()).filter(name => name.startsWith('.claude-')).map(name => join(homedir(), name)), + ...(process.env.CLAUDE_CONFIG_DIR ? [process.env.CLAUDE_CONFIG_DIR] : []), +]; const resolved = (path: string) => { try { return realpathSync(path); } catch { return resolve(path); } }; const within = (child: string, parent: string) => child === parent || child.startsWith(`${parent}/`); @@ -39,7 +46,7 @@ export function disposableConfig(requested: string | undefined): string { } const configDir = disposableConfig(process.env.NMSH_PROBE_CONFIG_DIR); if (!market) throw new Error('Set NMSH_PROBE_MARKET'); -const realSettings = ['.claude', '.claude-account1', '.claude-account2'].map(dir => join(homedir(), dir, 'settings.json')).filter(existsSync); +const realSettings = [...new Set(REAL_IDENTITIES.map(dir => join(resolved(dir), 'settings.json')))].filter(existsSync); const hashes = () => realSettings.map(path => createHash('sha256').update(readFileSync(path)).digest('hex')).join(','); const before = hashes(); const env = {...process.env, CLAUDE_CONFIG_DIR: configDir}; From ebdb1ad9096274fc6a1f4cc064b73d8bdf9260fa Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:44:16 +0530 Subject: [PATCH 015/249] Test comment: describe the two-account fixture without personal names --- tests/claudePlugins.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/claudePlugins.test.ts b/tests/claudePlugins.test.ts index 18534f7d..a3bc466c 100644 --- a/tests/claudePlugins.test.ts +++ b/tests/claudePlugins.test.ts @@ -19,7 +19,7 @@ const root = mkdtempSync(join(tmpdir(), 'nmsh-plugins-')); test.after(() => rmSync(root, {recursive: true, force: true})); const write = (path: string, content: string) => { mkdirSync(join(path, '..'), {recursive: true}); writeFileSync(path, content); }; -/** Two launch identities, each with its own plugins folder, as claude-account1 and claude-account2 are laid out. */ +/** Two launch identities, each with its own plugins folder, as separate accounts are usually laid out. */ function plugin(account: string, name: string, files: Record): string { const dir = join(root, account, 'plugins', 'cache', 'market', name, '1.0.0'); for (const [file, content] of Object.entries(files)) write(join(dir, file), content); From 878e7163c306bd896b18a91f0849bd05efc34b47 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Sat, 10 Oct 2026 02:42:17 +0530 Subject: [PATCH 016/249] Input awareness: say when a command is waiting for you, and route typing the way it reads A command that waits for an answer no longer looks like any other running command. InputWatch, beside the PTY (session service or in-process), decides from positive evidence only: - the terminal's line settings (stty): echo off with line input is a hidden read; line input off with a prompt left open is single-key input (e2fsck's Fix?); both confirmed; - on Linux, the kernel's report of a blocked read of the terminal (confirmed), or of a block elsewhere (veto); - otherwise a question-shaped line left open, quiet output and an idle foreground group: "probably". Programs that own the terminal, builds, sleep, network waits, progress rewrites, statuses and log streams are never reported. One state reaches every surface: the live activity rows show the attention line and the open question (no extra rows, no PTY resize), the window title, /sessions, /resume, and one sticky cross-session notice per wait. A new input-state message and the "input" notice kind go only to frontends that list the feature. Hidden and single-key input goes straight to the program key by key (never shown, kept or logged), which also fixes the stray Enter that answered the next single-key question with its default and the password drawn in the composer. Keys typed after the question but before detection are handed over; a line reply left unsent when the command ends is discarded, never run. Waiting time is measured apart from wall-clock duration, carried with the completing prompt, and recorded on the transcript record. --- CHANGELOG.md | 7 + docs/design/input-awareness.md | 150 ++++++++++ src/app/TerminalApp.ts | 131 +++++++-- src/output/OutputBuffer.ts | 4 + src/session/InProcessSessionClient.ts | 1 + src/session/InputWatch.ts | 248 ++++++++++++++++ src/session/SessionClient.ts | 3 + src/session/SessionNotices.ts | 29 +- src/session/SessionProtocol.ts | 57 +++- src/session/SessionService.ts | 71 ++++- src/session/SocketSessionClient.ts | 8 +- src/session/StreamBacklog.ts | 2 +- src/session/inputState.ts | 51 ++++ src/session/liveStatus.ts | 10 +- src/session/openLine.ts | 91 ++++++ src/session/terminalProbe.ts | 168 +++++++++++ src/sessions/LiveSessionView.ts | 4 +- src/sessions/ResumeBrowser.ts | 16 +- src/sessions/TranscriptStore.ts | 2 + src/shell/ShellProtocol.ts | 3 + src/shell/ShellSession.ts | 36 ++- src/status/inputStatus.ts | 88 ++++++ src/terminal/keyBytes.ts | 33 +++ tests/fixtures/askyn.py | 24 ++ tests/inputAwareness.test.ts | 392 ++++++++++++++++++++++++++ tests/inputAwarenessLive.test.ts | 233 +++++++++++++++ 26 files changed, 1803 insertions(+), 59 deletions(-) create mode 100644 docs/design/input-awareness.md create mode 100644 src/session/InputWatch.ts create mode 100644 src/session/inputState.ts create mode 100644 src/session/openLine.ts create mode 100644 src/session/terminalProbe.ts create mode 100644 src/status/inputStatus.ts create mode 100644 src/terminal/keyBytes.ts create mode 100755 tests/fixtures/askyn.py create mode 100644 tests/inputAwareness.test.ts create mode 100644 tests/inputAwarenessLive.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ae92aa6..f7aa1334 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Added +- **Input awareness:** a command that waits for an answer is no longer just "Running". NMSh reads the terminal's own state, without touching the program. A password or other no-echo read, or a single-key question such as `e2fsck`'s `Fix?`, is confirmed; on Linux, any blocking read of the terminal is confirmed too. An ordinary line question left open by an idle process is shown as "Probably waiting for input". The question stays visible beside the composer however far the transcript has scrolled, and the window title, `/sessions`, `/resume` and a cross-session notice show the wait. Hidden and single-key input goes straight to the program, keystroke by keystroke, and is never shown or kept. A reply left unsent when the command ends is discarded, never run as a shell command. Waiting time is measured separately from wall-clock time and reported on completion. See `docs/design/input-awareness.md`. + +### Fixed +- **Single-key prompts:** answering a single-key question through the composer no longer sends a stray Enter that answered the next question with its default. +- **Passwords:** text typed at a password prompt is no longer drawn in the composer. + ## [0.18.0] - 2026-10-09 Context Engine platform, Status Strip 2.0, managed Claude Code, worktrees and a read-only GitHub workspace: demand-driven context modules and Context Packs, `/strip` and `/modules`, `/claude` with structured permissions and questions, `/mods`, `/worktrees`, `/github`, terminal-title ownership, and tmux, passthrough and cursor fixes. diff --git a/docs/design/input-awareness.md b/docs/design/input-awareness.md new file mode 100644 index 00000000..19771bb6 --- /dev/null +++ b/docs/design/input-awareness.md @@ -0,0 +1,150 @@ +# Input awareness: when a command needs you + +A running command that waits for an answer (`e2fsck`'s `Fix?`, a password, `rm -i`, a `read` prompt) used to look +like any other running command: `Running · 30s`. NMSh now tells the two apart, shows the question where it stays in +view, routes typing the way the program reads it, and keeps waiting time separate from execution time. + +Verified on macOS 27 (zsh 5.9, Bash 5.3, Fish 4.9) on 2026-10-10. The Linux kernel layer is implemented and unit-tested +against the kernel's documented `/proc` formats, but it has not been run on a Linux host in this change. + +## Principles + +- **NMSh stays a frontend over the real shell.** It reads the terminal's own state and the process table. It never + pauses, signals or writes to a program to find out, and it never answers for you. +- **One authoritative state.** `InputWatch` runs beside the PTY: in the session service, or in-process when there is + no service. It publishes one `InputState` that every surface renders: the live activity line, the composer, the + window title, `/sessions`, `/resume` and cross-session notices. No surface infers anything on its own. +- **Silence is not evidence.** A quiet process might be sleeping, computing or waiting on the network. A request needs + positive evidence, and its confidence is shown in words. + +## What NMSh can know, and how + +| Layer | Signal | Platforms | Confidence | +|---|---|---|---| +| Kernel | A foreground thread is blocked in `read()` on this terminal (`/proc/PID/task/TID/syscall` plus the fd's link). If every thread is blocked elsewhere, that vetoes the other layers. | Linux | confirmed | +| Terminal line settings | `stty -a` on the PTY device: echo off with line input on means a hidden line (password). Line input off with a prompt left open on the cursor's line means single keys (`e2fsck`, `read -k`, `read -n1`). | macOS, Linux | confirmed | +| Prompt evidence | A question-shaped line left open (ends in `?`, `:`, `]`, `)`, `>`), written once and never rewritten or cursor-drawn, output quiet for 0.7 s or more, and the foreground group idle across two samples (nothing running on a CPU, no CPU time gained). | macOS, Linux | likely | + +Raw mode alone is never a request: spinners and terminal proxies (`docker run -it`) set it without asking anything. A +program that owns the terminal is never reported, because it draws and reads its own interface. That covers the +alternate screen, bracketed paste, mouse or focus reporting, the kitty keyboard protocol, and NMSh's known full-screen +commands. + +**Reliable:** password and other no-echo reads (`sudo`, `ssh`, `getpass`, `read -s`); single-key questions (`e2fsck`, +`read -k1`, `read -n1`, menu libraries that read keys); on Linux, any blocking read of the terminal, including prompts +that end with a newline. + +**Inferred (shown as "Probably waiting for input"):** ordinary line reads on macOS, such as `read -p`, `rm -i`, Python +`input()` and `apt`'s `[Y/n]`. macOS exposes no wait channel without privileges (`ps -o wchan` is empty for every +process), so the open question plus an idle process is the best available evidence. + +**Not detected generically:** +- Line reads on macOS whose prompt ends with a newline, or that print no prompt at all. +- Programs reading through `poll`/`select` on macOS. +- Programs that own the terminal. Fish 4's own `read` is one: it is fish's line editor, with bracketed paste and OSC 133 + marks, so NMSh hands it the terminal and fish answers natively. It is not reported as waiting. + +In all of these cases NMSh shows "Running", as before. + +**False-positive guards:** +- **Builds:** busy CPU or a process in state `R`. +- **`sleep`, network waits, background tasks:** no question left open. +- **Progress bars:** a carriage-return rewrite marks the line unreliable. +- **Status lines:** `Downloading...` is not question-shaped, and cursor-drawn lines are ignored. +- **Log streams:** no open line, and probes are rate-limited. +- **Questions printed as ordinary output:** later output ends the wait at once. +- **Interactive full-screen or inline programs:** they own the terminal. + +### Cost + +Probes start only after a running command's output goes quiet: +- **First look:** 150 ms after quiet with a line left open, 400 ms otherwise. +- **Then:** at 1 s and 2.5 s, every 3 s, and every 10 s after a minute of quiet. +- **Closed line:** at most one look every 2 s. + +Each look runs `/bin/stty -a` and `/bin/ps` (plus `/proc` reads on Linux) with fixed arguments and a 1.5 s timeout. A +probe that started before newer output is discarded. + +## States and time + +- **Running:** no credible request. The line keeps its shimmer and adds `· waited 18s` once the command has waited. +- **Waiting for input** (◆, accent and weight) or **Probably waiting for input** (◇): the attention line plus the open + question. +- **Running again:** after an answer (a key in key mode, Enter otherwise, or Ctrl+C or Ctrl+D), or when the program + writes again. +- **Completed, failed or interrupted:** unchanged. + +`durationMs` stays the whole wall-clock time. Waiting is measured separately: +- **Start:** when the program wrote the prompt it waits on. +- **End:** at the answer, at new output, or at completion. +- **Evidence that simply disappears:** if a read times out silently, the wait ends at the look that saw it gone, so + that one boundary is accurate to within a probe interval. + +Intervals are summed per command and carried with the completing prompt (`inputWaitMs`, `inputWaits`), so they survive +detach and replay. They are recorded on the transcript record, and the completion row reads +`✔ Completed · 1m 25s · 18s waiting for input (2 prompts) · 15:42`. Active time is not shown: waiting is the only +boundary NMSh observes. + +## Presentation + +The activity region already reserves two rows while a command runs (the working line and a spacer). A wait uses both, +so nothing new is reserved and the PTY never resizes: + +``` +◆ Waiting for input · e2fsck · 18.0s · 1m 40s total + Padding at end of inode bitmap is not set. Fix? · each key goes straight to e2fsck +❯ Keys go straight to e2fsck +``` + +- **The question stays in view** however far the transcript has scrolled or folded. +- **Narrow widths:** the line drops detail first (total time, then program name); the question outranks the hint. +- **No animation:** no shimmer and no flashing. The timer only counts. +- **NO_COLOR:** keeps the weight and the glyph. +- **Safe glyphs:** `!` for confirmed and `?` for likely. +- **Window title:** reads `◆ e2fsck needs input · project`, so a background tab shows it. + +## Input ownership + +- **Line input** (confirmed or likely): the composer is the line editor, as before. Enter sends the line to the running + program, never to the shell. A reply that is still unsent when the command ends is discarded with a note. It never + runs as the next command. +- **Hidden lines and single keys** (confirmed): each key goes straight to the program as the bytes a plain terminal + sends, so line-discipline editing, Enter-as-default and no-echo all behave natively. + - The composer shows only where typing goes. A draft typed before the question appeared stays hidden and untouched. + - Keys typed after the question appeared, in the moment before NMSh knew how the program reads, are handed over as a + terminal would have delivered them. + - Ctrl+C, Ctrl+D and Ctrl+Z keep their meaning, and PageUp/PageDown and the wheel still scroll the transcript. + - NMSh gestures and pointer events are never sent. + - Nothing typed is shown, journaled, put in history or written to the key-debug log. +- **There are no answer buttons.** NMSh never submits an answer on your behalf. + +## Other sessions + +The service is the one owner of every session's state: +- **`/sessions` and `/resume`** lead the row with `◆ Waiting for input` and the question. +- **Cross-session notice:** a wait that lasts 3 s becomes one sticky notice in other windows, for example + `Mango · e2fsck is waiting for input · 2m · /resume`. It is withdrawn when the wait ends or the session is focused. +- **Returning:** go back through `/resume`, which attaches. Attaching shows the waiting question; nothing is answered + automatically. +- **Older frontends:** the frontend lists `input-state` in its hello. An older frontend gets neither the new message + nor the new notice kind, so it never rejects a frame. + +## Managed agents + +Managed Claude sessions (`/claude`) do not run in a PTY. Their permission requests and questions arrive as structured +events, and the agent view already marks them with the same ◆ attention. Input awareness never parses an agent's +screen. A coding agent run as a shell command (`claude`, `codex`) owns the terminal, so NMSh hands it over and reports +it through its own attention signals (bell, OSC 9/777), not as "waiting for input". + +## Files + +- `src/session/InputWatch.ts`: the decision and timing (one per session, beside the PTY). +- `src/session/terminalProbe.ts`: read-only `stty`, `ps` and `/proc` facts. +- `src/session/openLine.ts`: the open line and the prompt shape. +- `src/session/inputState.ts`: the shared state. +- Protocol, service and clients: `SessionProtocol.ts` (`input-state`, `CLIENT_FEATURES`), `SessionService.ts`, + `SocketSessionClient.ts`, `InProcessSessionClient.ts`. +- `src/status/inputStatus.ts`: wording and painting. +- `src/terminal/keyBytes.ts`: direct key bytes. +- Tests: `tests/inputAwareness.test.ts` (unit) and `tests/inputAwarenessLive.test.ts` (real PTY: zsh, Bash, Fish, detach + and reattach, other windows, narrow, NO_COLOR and Safe). The harmless e2fsck-style fixture is `tests/fixtures/askyn.py`. diff --git a/src/app/TerminalApp.ts b/src/app/TerminalApp.ts index 2218896d..bd48858d 100644 --- a/src/app/TerminalApp.ts +++ b/src/app/TerminalApp.ts @@ -178,6 +178,9 @@ import {TaskProgress} from '../status/TaskProgress.js'; import {renderStatusStrip, stripStatsFromFacts, stripVisible, type StripModuleItem} from '../status/StatusStrip.js'; import {PATH_DISPLAY_LEVELS} from '../prompt/pathDisplay.js'; import {completedActivity, liveActivityParts} from '../status/activity.js'; +import {completionWaitFact, directInputPlaceholder, inputActivityRows, inputGlyph, inputProgram, waitedDetail} from '../status/inputStatus.js'; +import {directInput, type InputState, type InputTiming} from '../session/inputState.js'; +import {directKeyBytes} from '../terminal/keyBytes.js'; import {extractFacts} from '../status/adapters.js'; import {focusForeground, foreground, background, UI_COLORS, lazyForeground} from '../ui/palette.js'; import {AgentActivityStore} from '../agents/AgentActivityStore.js'; @@ -719,6 +722,16 @@ export class TerminalApp { private readonly effects = new EffectState(); private presentationFrame?: {frame: TerminalFrame; plan: ScreenPlan; agentCaretRow?: number}; private activityAnimationNow = Date.now(); + /** The running command's input state, as the session's InputWatch reports it; never guessed here. */ + private inputState?: InputState; + /** Waiting time the shell's completing prompt carried for the command that just finished. */ + private completedInputTiming?: InputTiming; + /** + * A composer draft started while a command runs: when, and whether a request for input was already shown (an + * answer, discarded rather than run if the command ends first). Drafts started before the command's latest output + * are type-ahead for the shell and stay in the composer. + */ + private runningDraft?: {startId: number; at: number; answering: boolean}; /** One pending timeout at a time drives the welcome cat's occasional blink. */ private welcomeBlinkTimer?: () => void; private welcomeBlinkCount = 0; @@ -790,11 +803,13 @@ export class TerminalApp { } // Host OSC 7 / OSC 133: a projection of this authoritative marker, never the other way round. this.hostSemantics.prompt(marker.cwd, marker.exitCode); + this.completedInputTiming = marker.inputWaits ? {waitedMs: marker.inputWaitMs ?? 0, waits: marker.inputWaits} : undefined; this.onShellPrompt(marker.exitCode, marker.cwd, stamp.at); } }); this.session.on('exec', (command, stamp) => { if (this.inStream(stamp)) { this.hostSemantics.exec(); this.onShellExec(command, stamp.at, stamp.historyAllowed); } }); this.session.on('replayed', summary => this.finishReplay(summary)); + this.session.on('inputState', state => this.onInputState(state)); this.session.on('inputRejected', (data, submission) => this.onInputRejected(data, submission)); this.session.on('startup', tail => { this.startupTail = tail; @@ -1014,6 +1029,7 @@ export class TerminalApp { return; } this.commandModes.reset(); + this.inputState = undefined; const startId = this.output.beginCommand(command, this.formatCommandAnsi(command, null), mode => this.onActiveModeChange(mode), this.historicalContext(this.shellCwd, this.context, command)); this.tapActivityObserver.reset(this.output.activeOutputStartId ?? startId); @@ -1120,7 +1136,8 @@ export class TerminalApp { private readonly replyRouter = new ReplyRouter(); private readonly onInput = (data: string): void => { - if (process.env.NMSH_DEBUG_KEYS === '1') { + // Key debugging never records what a program reads with echo off (or single keys it asked for). + if (process.env.NMSH_DEBUG_KEYS === '1' && !this.directInputRequest()) { const hex = Array.from(Buffer.from(data)).map(b => b.toString(16).padStart(2, '0')).join(' '); const escaped = JSON.stringify(data); appendFileSync('/tmp/nmsh-key-debug.log', `RAW hex=${hex} escaped=${escaped}\n`); @@ -1816,6 +1833,17 @@ export class TerminalApp { this.historyViewport.latest(); return; } + // A program reading a hidden line or single keys gets each key as it is typed, as in any terminal: the + // composer neither shows nor keeps it, and Enter is a key for the program, never a shell submission. + if (this.directInputRequest()) { + this.sendDirectKey(key); + return; + } + // Typing that starts while a command waits for a line is an answer: if the command ends before it is sent, it + // is discarded rather than left to run as the next shell command. + if (this.running && !this.editor.text && (key.kind === 'text' || key.kind === 'paste')) { + this.runningDraft = {startId: this.running.startId, at: Date.now(), answering: Boolean(this.inputState?.request)}; + } // Flow keeps the composer in the document: editing while scrolled back // returns to it first. Scrolling and mouse navigation alone never do. if (this.promptConfiguration.composerPosition === 'flow' && this.historyViewport.detached && FLOW_EDIT_KEYS.has(key.kind)) { @@ -2104,6 +2132,7 @@ export class TerminalApp { } else { const input = this.editor.text; this.editor.clear(); + this.runningDraft = undefined; this.session.write(`${input}\r`); return; } @@ -2606,6 +2635,8 @@ export class TerminalApp { const contextAtSubmission = this.context; this.effects.cancel(); this.commandModes.reset(); + this.inputState = undefined; + this.runningDraft = undefined; const startId = this.output.beginCommand(command, this.formatCommandAnsi(command, null), (mode) => { if (mode === 'PASSTHROUGH' && !this.passthrough && !this.startupPending) { this.cancelPresentation(); @@ -2828,10 +2859,10 @@ export class TerminalApp { rows.forEach((row, index) => { const selected = index === browser.selectedIndex; const marker = row.current ? (safe ? '*' : '●') : row.session.state === 'detached' ? (safe ? '-' : '◌') : (safe ? 'o' : '○'); - const color = row.state === 'failed' ? ERROR : row.state === 'attention' ? ACCENT : row.state === 'completed' ? SUCCESS : SECONDARY; + const color = row.state === 'failed' ? ERROR : row.state === 'attention' || row.state === 'input' || row.state === 'inputLikely' ? ACCENT : row.state === 'completed' ? SUCCESS : SECONDARY; const who = row.agent ? `${agentColor(row.agent.color)}${safe ? row.agent.safeGlyph : row.agent.glyph} ${row.agent.short}${RESET} ` : ''; out.push(truncateAnsi(`${selected ? `${ACCENT}›` : ' '} ${marker} ${PRIMARY}${(row.current ? 'this' : `#${row.ordinal}`).padEnd(5)}${RESET}${SECONDARY}${row.shell.padEnd(5)}${RESET} ` - + `${color}${row.stateLabel.padEnd(16)}${RESET}${who}${focusForeground(selected)}${row.summary}${RESET}`, columns)); + + `${color}${row.stateLabel.padEnd(18)}${RESET}${who}${focusForeground(selected)}${row.summary}${RESET}`, columns)); }); const confirming = browser.live.find(session => session.id === browser.confirmKill); if (confirming) out.push('', truncateAnsi(`${ERROR} Kill the live session in ${confirming.cwd}? Its shell and anything running in it will end. Enter kill · Esc cancel${RESET}`, columns)); @@ -3151,6 +3182,8 @@ export class TerminalApp { if (completedRecord) { completedRecord.startedAt = command.startedAt; completedRecord.durationMs = Math.max(0, elapsed); + // Wall-clock duration stays whole; the time spent waiting for the person is a separate, measured fact. + if (this.completedInputTiming) { completedRecord.inputWaitMs = this.completedInputTiming.waitedMs; completedRecord.inputWaits = this.completedInputTiming.waits; } completedRecord.historyEligible = command.historyAllowed === 1 && !isPrivateCommand(command.command, ignorePatternFromEnv()); this.directoryQuery = undefined; this.directoryQueryAbort?.abort(); @@ -3170,6 +3203,7 @@ export class TerminalApp { const failure = isInterrupted ? undefined : classifyShellFailure(command.command, exitCode, outputText); if (failure) parts.main = parts.main.replace('Command failed', failure === 'command-not-found' ? 'Command not found' : 'Shell syntax error'); else if (agent) parts.main = agentCompletionText(agent.id, elapsed, exitCode, isInterrupted); + parts.main += completionWaitFact(this.completedInputTiming); this.output.setCompletionLifecycle(`${parts.main}${parts.detail}`); const rowStyle = isInterrupted ? STOPPED : (exitCode !== 0 ? ERROR : SUCCESS); this.output.addHistoryLine(`${rowStyle}${parts.main}${SECONDARY}${parts.detail}${RESET}`); @@ -3178,6 +3212,17 @@ export class TerminalApp { if (known) this.output.addHistoryLine(`${INFO}${known}${RESET}`); } this.running = undefined; + this.inputState = undefined; + this.completedInputTiming = undefined; + // An answer typed for this command but never sent must not become the next shell command. + const draft = this.runningDraft; + this.runningDraft = undefined; + if (draft?.answering) { + if (this.editor.text) { + this.editor.clear(); + this.output.addFrontendInteraction(commandWords(command.command)[0] ?? 'command', 'The command finished before your reply was sent; the reply was discarded, not run.', INFO); + } + } if (!this.replaying && !command.cleared && completedRecord) { const interrupted = command.interrupted || exitCode === 130; const now = Date.now(); @@ -4013,12 +4058,13 @@ export class TerminalApp { live.forEach((session, index) => { const selected = index === browser.selectedIndex; const state = liveRowState(session, now); - const stateColor = state === 'failed' ? ERROR : state === 'attention' ? ACCENT : state === 'completed' ? SUCCESS : state === 'active' ? PRIMARY : SECONDARY; + const stateColor = state === 'failed' ? ERROR : state === 'attention' || state === 'input' || state === 'inputLikely' ? ACCENT : state === 'completed' ? SUCCESS : state === 'active' ? PRIMARY : SECONDARY; const agent = liveRowAgent(session); // Agent color only when identity is proven and color is allowed; generic otherwise. const who = agent ? `${agentColor(agent.color)}${safe ? agent.safeGlyph : agent.glyph} ${agent.short}${RESET} ` : ''; const signature = signatureAccent(session.signature); - rows.push(truncateAnsi(`${selected ? ACCENT : SECONDARY}${selected ? '›' : ' '} ${stateColor}${(safe ? '*' : '●')} ${LIVE_ROW_LABELS[state].padEnd(16)}${RESET}${signature ? `${foreground(signature)}${safe ? '+' : '◆'}${RESET} ` : ''}${who}` + const stateMark = state === 'input' ? (safe ? '!' : '◆') : state === 'inputLikely' ? (safe ? '?' : '◇') : (safe ? '*' : '●'); + rows.push(truncateAnsi(`${selected ? ACCENT : SECONDARY}${selected ? '›' : ' '} ${stateColor}${stateMark} ${LIVE_ROW_LABELS[state].padEnd(17)}${RESET}${signature ? `${foreground(signature)}${safe ? '+' : '◆'}${RESET} ` : ''}${who}` + `${selected ? ACCENT : SECONDARY}${describeLiveRow(session, now)}${RESET}`, columns)); }); rows.push('', `${SUBTLE} ARCHIVED${RESET}`); @@ -4540,7 +4586,9 @@ export class TerminalApp { if (mode === 'off') return undefined; const project = this.context.project && this.context.project !== '…' ? this.context.project : basename(this.shellCwd) || this.shellCwd; const session = this.sessionId ? sessionTitleName(this.sessionIdentity, this.noticeLabels.get(this.sessionId)) : undefined; - const running = this.running && !this.passthrough ? commandWords(this.running.command)[0] : undefined; + const request = this.running && !this.passthrough ? this.inputState?.request : undefined; + const running = request ? `${inputGlyph(request)} ${inputProgram(request, this.running!.command)} needs input` + : this.running && !this.passthrough ? commandWords(this.running.command)[0] : undefined; return composeTitle(mode, project, session, running); } @@ -6560,9 +6608,10 @@ export class TerminalApp { // Expired notices never paint, even between polls. const notices = this.noticeView.notices.filter(notice => noticeVisible(notice, now)); const safe = getCurrentGlyphMode() === 'safe'; - const symbols = {done: safe ? '+' : '✦', attention: safe ? '!' : '◆', failed: safe ? 'x' : '×', ended: safe ? '-' : '○', long: safe ? '~' : '◷'}; + const symbols = {done: safe ? '+' : '✦', attention: safe ? '!' : '◆', failed: safe ? 'x' : '×', ended: safe ? '-' : '○', long: safe ? '~' : '◷', input: safe ? '?' : '◇'}; const rows = notices.map(notice => { const parts = describeNotice(notice, this.noticeLabels.get(notice.sessionId) ?? sessionLabel(notice.sessionId), now); + if (parts.symbol === 'input') return truncateAnsi(`${ACCENT}${inputGlyph({confidence: notice.confidence === 'likely' ? 'likely' : 'confirmed'})}${RESET} ${SECONDARY}${parts.text}${RESET}`, columns); const color = parts.symbol === 'failed' ? ERROR : parts.symbol === 'attention' ? ACCENT : parts.symbol === 'done' ? SUCCESS : SECONDARY; return truncateAnsi(`${color}${symbols[parts.symbol]}${RESET} ${SECONDARY}${parts.text}${RESET}`, columns); }); @@ -8530,8 +8579,10 @@ export class TerminalApp { } } + const direct = this.directInputRequest(); const inputRows = input.rows.map(row => { const prefix = row.prefix.startsWith(GLYPHS.prompt) ? `${ACCENT}${GLYPHS.prompt}${RESET}${row.prefix.slice(GLYPHS.prompt.length)}` : row.prefix; + if (direct) return row === input.rows[0] ? truncateAnsi(`${prefix}${SUBTLE}${directInputPlaceholder(direct, inputProgram(direct, this.running!.command))}${RESET}`, this.inputColumns(columns)) : ''; let textStyled = ''; const glyphsInRow = graphemes(row.text); for (let i = 0; i < glyphsInRow.length; i++) { @@ -8585,11 +8636,7 @@ export class TerminalApp { ); }), ...(menuOverflow ? [renderCompletionMore(hiddenBelow, suggestionView.start, columns)] : [])]); // The spacer sits between the newest output and the activity line in both positions. - case 'activity': { - if (!this.running) return []; - const activity = truncateAnsi(this.currentActivity(), columns); - return plan.composerPosition === 'top' ? ['', activity] : [activity, '']; - } + case 'activity': return this.activityRows(plan, columns); case 'composerBorder': return [this.composerEdgeRow('composerBorder', plan, columns, now)]; case 'prompt': return [promptLine]; case 'input': return inputRows; @@ -8793,8 +8840,7 @@ export class TerminalApp { const rows = [...frame.rows]; for (const region of plan.regions) { if (region.kind === 'activity' && this.running) { - const line = truncateAnsi(this.currentActivity(), frame.columns ?? 80); - const content = plan.composerPosition === 'top' ? ['', line] : [line, '']; + const content = this.activityRows(plan, frame.columns ?? 80); for (let index = 0; index < region.height; index++) rows[region.top + index] = content[index] ?? ''; } // Live composer divider lines move with Chroma only when Divider lines follow Chroma. @@ -8864,6 +8910,55 @@ export class TerminalApp { } } + /** + * The live activity region (two rows, always reserved while a command runs). Ordinarily the working line and a + * spacer; while the command waits for input, the attention line and the prompt it left open, so the question + * stays in view however far the transcript scrolled or folded. Nothing extra is reserved and the PTY never resizes. + */ + private activityRows(plan: ScreenPlan, columns: number): string[] { + if (!this.running) return []; + const request = this.inputState?.request; + // A wait reads top to bottom (headline, then the question) wherever the composer is docked. + if (request) return inputActivityRows({...this.inputState!, request}, this.running.command, this.running.startedAt, this.activityAnimationNow, columns).map(row => truncateAnsi(row, columns)); + const activity = truncateAnsi(this.currentActivity(), columns); + return plan.composerPosition === 'top' ? ['', activity] : [activity, '']; + } + + /** Keys go straight to the program (a hidden line or single keys) rather than through the composer. */ + private directInputRequest() { + const request = this.inputState?.request; + return this.running && !this.passthrough && !this.startupPending && directInput(request) ? request : undefined; + } + + private onInputState(state: InputState): void { + // A state for a finished run (a late message) never resurrects it. + if (!this.running) return; + this.inputState = state; + const request = state.request; + const draft = this.runningDraft; + // Keys typed after the program wrote its question, in the moment before NMSh could tell how it reads, were + // meant for it: a terminal would already have delivered them. Hand them over now instead of hiding them. + if (request && directInput(request) && draft && draft.startId === this.running.startId && draft.at >= request.since && this.editor.text) { + const typed = this.editor.text; + this.editor.clear(); + this.runningDraft = undefined; + this.session.write(typed); + } else if (request && draft && draft.at >= request.since && this.editor.text) draft.answering = true; + this.render(); + } + + /** Forward one key to the waiting program as the bytes a terminal sends; nothing is kept, shown or logged. */ + private sendDirectKey(key: Key): void { + if (key.kind === 'interrupt') { + if (this.running) this.running.interrupted = true; + this.session.interrupt(); + return; + } + if (key.kind === 'eof') { this.session.endInput(); return; } + const bytes = directKeyBytes(key); + if (bytes) this.session.write(bytes); + } + private currentActivity(): string { if (!this.running) return ''; const elapsed = this.activityAnimationNow - this.running.startedAt; @@ -8875,7 +8970,7 @@ export class TerminalApp { const still = !this.decorativeMotionAllowed() || sweepStill(this.promptConfiguration); const {cells, style} = liveActivityPaint(parts.phrase, this.promptConfiguration, this.activityAnimationNow, still); const phrase = sweepCells(cells, animationElapsed * (isActive ? 1.4 : 1), style, colorLevel(), !this.decorativeMotionAllowed()); - return `${phrase}${SECONDARY}${parts.duration}${RESET}`; + return `${phrase}${SECONDARY}${parts.duration}${waitedDetail(this.inputState, this.activityAnimationNow, elapsed)}${RESET}`; } private jumpAffordance(columns: number): string { @@ -8917,9 +9012,11 @@ export class TerminalApp { } private layoutEditorInput(columns: number, maxVisibleRows = Number.POSITIVE_INFINITY) { + // Direct input: the composer shows where typing goes, never a draft, so the caret sits at the start. + const direct = Boolean(this.directInputRequest()); return layoutInput( - this.editor.displayText, - this.editor.displayCursorIndex, + direct ? '' : this.editor.displayText, + direct ? 0 : this.editor.displayCursorIndex, this.inputColumns(columns), maxVisibleRows, this.railInputPrefix(columns), diff --git a/src/output/OutputBuffer.ts b/src/output/OutputBuffer.ts index 2760b5da..47b5d362 100644 --- a/src/output/OutputBuffer.ts +++ b/src/output/OutputBuffer.ts @@ -39,7 +39,11 @@ export interface CompletedCommand { /** Only explicitly eligible commands enter command history; transcript retention is separate. */ historyEligible?: boolean; startedAt?: number; + /** Wall-clock time from start to completion, waiting included. */ durationMs?: number; + /** Of durationMs, the time the command spent waiting for input, and how many waits (see InputWatch). */ + inputWaitMs?: number; + inputWaits?: number; command: string; output: string; lifecycleText: string; diff --git a/src/session/InProcessSessionClient.ts b/src/session/InProcessSessionClient.ts index d5618d3c..768c3582 100644 --- a/src/session/InProcessSessionClient.ts +++ b/src/session/InProcessSessionClient.ts @@ -36,6 +36,7 @@ export class InProcessSessionClient extends EventEmitter im shell.on('exec', (command, historyAllowed) => { this.running = true; this.emit('exec', command, historyAllowed === undefined ? NO_STAMP : {historyAllowed}); }); shell.on('startup', tail => this.emit('startup', tail)); shell.on('inputRejected', (data, submission) => this.emit('inputRejected', data, submission)); + shell.on('inputState', state => this.emit('inputState', state)); shell.on('exit', event => this.emit('exit', event)); } diff --git a/src/session/InputWatch.ts b/src/session/InputWatch.ts new file mode 100644 index 00000000..59d9ed09 --- /dev/null +++ b/src/session/InputWatch.ts @@ -0,0 +1,248 @@ +import {EventEmitter} from 'node:events'; +import {shouldPassthrough} from '../passthrough/PassthroughPolicy.js'; +import {OpenLine, promptShaped, promptText} from './openLine.js'; +import {AlternateScreenTracker} from './TerminalModes.js'; +import type {ForegroundProcess, TerminalProbe, TerminalSample} from './terminalProbe.js'; +import {type InputMode, type InputRequest, type InputState} from './inputState.js'; + +/** + * Watches one session's running command for a credible request for input. Lives beside the PTY (in the session + * service, or in-process), so every frontend and /sessions read the same state. + * + * Evidence, strongest first; nothing is concluded from silence alone: + * 1. Linux: the kernel reports a foreground thread blocked in read() on this terminal → confirmed. + * 2. The terminal's line settings: echo off with line input (a password read) → confirmed hidden input; line input + * off with a prompt left open on the cursor's line (e2fsck's "Fix? ", `read -k`) → confirmed key input. + * Raw mode alone is not enough: spinners and terminal proxies (docker -it) set it without asking anything. + * 3. Ordinary line input: a question-shaped line left open, output quiet, the foreground group idle across two + * samples (no CPU, nothing running) → likely. A long build, `sleep`, a stalled download or a log stream does + * not leave a question open, rewrites its line, or is busy. + * A program that owns the terminal (full screen, or input modes such as bracketed paste) draws and reads its own + * interface; it is never reported as waiting. The kernel saying the group is blocked elsewhere vetoes 2 and 3. + * + * Observation only: probes read the terminal's settings and the process table, never input, and never pause, + * signal or write to the program. + */ +export interface InputWatchOptions { + probe?: TerminalProbe; + /** The terminal device path, once known. */ + tty?: () => string | undefined; + now?: () => number; + /** Timer seam for tests. */ + setTimer?: (callback: () => void, ms: number) => () => void; +} + +interface InputWatchEvents { + change: [InputState]; +} + +/** Quiet time before the first look: short when a line was left open (a prompt is likely), longer otherwise. */ +const FIRST_LOOK_OPEN_MS = 150; +const FIRST_LOOK_MS = 400; +/** Later looks back off: a long quiet command costs a few cheap probes a minute. */ +const LOOKS_MS = [600, 1200, 2500]; +const STEADY_LOOK_MS = 3000; +const SLOW_LOOK_AFTER_MS = 60_000; +const SLOW_LOOK_MS = 10_000; +/** + * With no line left open, only a hidden-line read (a password with no prompt text) or the kernel's report can say + * anything, so a slow stream (a log line every second) is looked at no more often than this. + */ +const CLOSED_LINE_INTERVAL_MS = 2000; +/** Ordinary line input is only "likely" after the output has been quiet this long. */ +const LIKELY_QUIET_MS = 700; +/** CPU share across two samples above which a foreground process is computing, not waiting. */ +const BUSY_SHARE = 0.25; + +const defaultTimer = (callback: () => void, ms: number) => { + const timer = setTimeout(callback, ms); + timer.unref?.(); + return () => clearTimeout(timer); +}; + +export class InputWatch extends EventEmitter { + private running?: {command: string; since: number}; + private readonly line = new OpenLine(); + private readonly screen = new AlternateScreenTracker(); + private request?: InputRequest; + private waitedMs = 0; + private waits = 0; + private lastActivity = 0; + private lastOutput = 0; + /** Bumped by every stream event; a probe that started before the latest event is stale. */ + private generation = 0; + private looks = 0; + private cancelTimer?: () => void; + private probing = false; + private lastProbeAt = -Infinity; + private previous = new Map(); + private disposed = false; + private readonly now: () => number; + private readonly setTimer: (callback: () => void, ms: number) => () => void; + + constructor(private readonly options: InputWatchOptions = {}) { + super(); + this.now = options.now ?? Date.now; + this.setTimer = options.setTimer ?? defaultTimer; + } + + get state(): InputState { + return {...(this.request ? {request: {...this.request}} : {}), timing: {waitedMs: this.waitedMs, waits: this.waits}}; + } + + /** Whether the current command's program owns the terminal (and so its own input). */ + get ownsTerminal(): boolean { + return Boolean(this.running && (this.screen.ownsTerminal || shouldPassthrough(this.running.command))); + } + + onExec(command: string, at = this.now()): void { + this.closeWait(at, false); + this.running = {command, since: at}; + this.waitedMs = 0; + this.waits = 0; + this.line.reset(); + this.screen.reset(); + this.previous.clear(); + this.lastOutput = at; + this.touch(at); + this.emit('change', this.state); + } + + onOutput(data: string, at = this.now()): void { + if (!this.running) return; + this.screen.observeModes(data); + this.screen.push(data); + this.line.push(data); + this.lastOutput = at; + // Output after the prompt: the program moved on (or asks again, which the next look sees). + if (this.request) this.closeWait(at, true); + this.touch(at); + } + + /** Bytes NMSh wrote to the program. An answer ends a wait; typing inside a hidden line does not. */ + onInput(data: string, at = this.now()): void { + if (!this.running) return; + const request = this.request; + if (request && (request.mode === 'key' || /[\r\n\u0003\u0004\u001a]/u.test(data))) this.closeWait(at, true); + this.touch(at); + } + + /** The command finished (the shell is back at its prompt): close any wait; the final timing stays readable. */ + onPrompt(at = this.now()): void { + if (!this.running) return; + this.closeWait(at, false); + this.running = undefined; + this.cancelTimer?.(); + this.cancelTimer = undefined; + this.generation += 1; + this.emit('change', this.state); + } + + dispose(): void { + this.disposed = true; + this.cancelTimer?.(); + this.cancelTimer = undefined; + this.removeAllListeners(); + } + + private closeWait(at: number, emit: boolean): void { + const request = this.request; + if (!request) return; + this.request = undefined; + this.waitedMs += Math.max(0, at - request.since); + this.waits += 1; + if (emit) this.emit('change', this.state); + } + + private touch(at: number): void { + this.lastActivity = at; + this.generation += 1; + this.looks = 0; + this.schedule(); + } + + private schedule(): void { + this.cancelTimer?.(); + this.cancelTimer = undefined; + if (this.disposed || !this.running || !this.options.probe) return; + const quiet = this.now() - this.lastActivity; + const planned = this.looks === 0 ? (this.line.open ? FIRST_LOOK_OPEN_MS : FIRST_LOOK_MS) + : LOOKS_MS[this.looks - 1] ?? (quiet > SLOW_LOOK_AFTER_MS ? SLOW_LOOK_MS : STEADY_LOOK_MS); + const delay = this.line.open ? planned : Math.max(planned, this.lastProbeAt + CLOSED_LINE_INTERVAL_MS - this.now()); + this.cancelTimer = this.setTimer(() => { this.cancelTimer = undefined; void this.look(); }, delay); + } + + private async look(): Promise { + if (this.disposed || !this.running || this.probing) return; + this.looks += 1; + if (this.ownsTerminal) { + if (this.request) this.closeWait(this.now(), true); + this.schedule(); + return; + } + const tty = this.options.tty?.(); + if (!tty || !this.options.probe) return; + const generation = this.generation; + this.probing = true; + this.lastProbeAt = this.now(); + let sample: TerminalSample | undefined; + try { sample = await this.options.probe({tty}); } catch { sample = undefined; } finally { this.probing = false; } + if (this.disposed || !this.running) return; + if (generation === this.generation && sample) this.apply(sample, this.now()); + this.schedule(); + } + + /** Decide from one sample. Exposed for tests through InputWatch.evaluate. */ + apply(sample: TerminalSample, now: number): void { + if (!this.running || this.ownsTerminal) return; + const busy = this.busy(sample.foreground, now); + const next = this.decide(sample, busy, now); + const current = this.request; + if (!next) { + // The evidence went away without input or output (a timed-out read, a restored terminal): the wait ended no + // later than now. + if (current) this.closeWait(now, true); + return; + } + const program = sample.foreground.find(item => item.command)?.command; + const request: InputRequest = {since: current?.since ?? Math.max(this.running.since, this.lastOutput), confidence: next.confidence, mode: next.mode, + ...(promptText(this.line.line) ? {prompt: promptText(this.line.line)} : {}), ...(program ? {program: program.slice(0, 64)} : {})}; + if (current && current.confidence === request.confidence && current.mode === request.mode && current.prompt === request.prompt + && current.program === request.program) return; + this.request = request; + this.emit('change', this.state); + } + + private decide(sample: TerminalSample, busy: boolean | undefined, now: number): {confidence: 'confirmed' | 'likely'; mode: InputMode} | undefined { + const modes = sample.modes; + const mode: InputMode = !modes ? 'line' : !modes.canonical ? 'key' : !modes.echo ? 'hidden' : 'line'; + if (sample.readingTerminal === true) return {confidence: 'confirmed', mode}; + // The kernel says every foreground thread is blocked on something other than the terminal. + if (sample.readingTerminal === false || busy === true) return undefined; + if (modes && modes.canonical && !modes.echo) return {confidence: 'confirmed', mode: 'hidden'}; + if (modes && !modes.canonical) return this.line.open ? {confidence: 'confirmed', mode: 'key'} : undefined; + // Ordinary line input: only a question left open, quiet output, and an idle group seen across two samples. + if (busy === false && this.line.open && promptShaped(this.line.line) && now - this.lastOutput >= LIKELY_QUIET_MS) return {confidence: 'likely', mode: 'line'}; + return undefined; + } + + /** true: computing; false: idle between this sample and the previous one; undefined: not yet known. */ + private busy(foreground: ForegroundProcess[], now: number): boolean | undefined { + if (!foreground.length) return undefined; + let known = !foreground.some(item => item.state.startsWith('R')) ? true : undefined; + let busy = known === undefined; + for (const item of foreground) { + const before = this.previous.get(item.pid); + if (item.cpuMs === undefined || !before) { if (known) known = false; continue; } + const elapsed = now - before.at; + if (elapsed > 0 && (item.cpuMs - before.cpuMs) / elapsed > BUSY_SHARE) busy = true; + } + this.remember(foreground, now); + if (busy) return true; + return known ? false : undefined; + } + + private remember(foreground: ForegroundProcess[], now: number): void { + this.previous = new Map(foreground.flatMap(item => item.cpuMs === undefined ? [] : [[item.pid, {cpuMs: item.cpuMs, at: now}] as const])); + } +} diff --git a/src/session/SessionClient.ts b/src/session/SessionClient.ts index a1244bb3..871ef1f2 100644 --- a/src/session/SessionClient.ts +++ b/src/session/SessionClient.ts @@ -3,6 +3,7 @@ import type {ShellMarker} from '../shell/ShellProtocol.js'; import type {TranscriptSession} from '../sessions/TranscriptStore.js'; import type {ShellId} from '../shell/adapters/ShellAdapter.js'; import type {ServiceFeature} from './SessionProtocol.js'; +import type {InputState} from './inputState.js'; /** Position of an event in a service session's stream, when it has one. */ export interface StreamStamp { @@ -24,6 +25,8 @@ export interface SessionClientEvents { startup: [string]; /** Input was not queued or written; the caller can restore it. */ inputRejected: [data: string, submission: boolean]; + /** The running command's input state changed (see InputWatch); the one source for every input surface. */ + inputState: [InputState]; /** The backlog sent after a reattach has been delivered. */ replayed: [{truncatedBytes: number}]; /** The managed shell ended. */ diff --git a/src/session/SessionNotices.ts b/src/session/SessionNotices.ts index 515ec38f..4f9effc8 100644 --- a/src/session/SessionNotices.ts +++ b/src/session/SessionNotices.ts @@ -19,7 +19,7 @@ import {formatAge, tildePath} from './sessionList.js'; * agent or program said. */ -export const NOTICE_KINDS = ['completed', 'failed', 'attention', 'ended', 'long-running'] as const; +export const NOTICE_KINDS = ['completed', 'failed', 'attention', 'ended', 'long-running', 'input'] as const; export type NoticeKind = typeof NOTICE_KINDS[number]; export interface SessionNotice { @@ -34,9 +34,13 @@ export interface SessionNotice { exitCode?: number; durationMs?: number; cwd?: string; + /** For an input notice: how sure the service is that the program waits for input (see InputWatch). */ + confidence?: string; } export const MAX_VISIBLE_NOTICES = 3; +/** A wait shorter than this is someone answering, not news for another window. */ +export const INPUT_NOTICE_DELAY_MS = 3000; /** A command still running after this long is worth one notice. */ export const LONG_RUNNING_MS = 15 * 60_000; /** @@ -70,7 +74,7 @@ export function noticeVisibleMs(notice: SessionNotice): number | undefined { case 'failed': return NOTICE_VISIBLE_MS.failed; case 'long-running': return NOTICE_VISIBLE_MS.longRunning; case 'ended': return notice.exitCode ? NOTICE_VISIBLE_MS.endedAbnormal : NOTICE_VISIBLE_MS.ended; - case 'attention': return undefined; + case 'attention': case 'input': return undefined; } } @@ -138,6 +142,21 @@ export class SessionNoticeTracker { this.current = {sessionId: this.sessionId, kind: 'attention', at, ...programIdentity(this.running.command, this.running.process)}; } + /** + * The running program has been waiting for input (since `since`) long enough to be news. One notice per wait; + * sticky until the wait ends, the session is focused, or a newer transition replaces it. + */ + onInputNeeded(since: number, confidence: string, program?: string): void { + if (!this.running || since <= this.clearedAt) return; + if (this.current?.kind === 'input' && this.current.at === since) return; + this.current = {sessionId: this.sessionId, kind: 'input', at: since, ...programIdentity(this.running.command, program ?? this.running.process), confidence}; + } + + /** The wait ended (answered, interrupted, or the program moved on): an input notice is no longer true. */ + onInputResolved(): void { + if (this.current?.kind === 'input') this.current = undefined; + } + /** Raise one long-running notice per run, lazily when someone lists sessions; no timers. */ checkLongRunning(now: number): void { const run = this.running; @@ -214,7 +233,7 @@ export function sessionLabel(sessionId: string, ordinal?: number): string { export interface NoticeLineParts { kind: NoticeKind; /** The glyph slot; the caller picks Nerd/Safe. */ - symbol: 'done' | 'attention' | 'failed' | 'ended' | 'long'; + symbol: 'done' | 'attention' | 'failed' | 'ended' | 'long' | 'input'; text: string; } @@ -232,6 +251,10 @@ export function describeNotice(notice: SessionNotice, label: string, now: number return {kind: notice.kind, symbol: 'failed', text: `${label} · ${who ?? 'command'} failed (exit ${notice.exitCode ?? '?'}) · ${age} ago`}; case 'attention': return {kind: notice.kind, symbol: 'attention', text: `${label} · ${who ?? 'program'} asked for attention · ${age} ago`}; + case 'input': + return {kind: notice.kind, symbol: 'input', text: `${label} · ${who ?? 'a command'} ${notice.confidence === 'likely' ? 'may be waiting for input' : 'is waiting for input'} · ${age} · /resume`}; + case 'input': + return {kind: notice.kind, symbol: 'input', text: `${label} · ${who ?? 'a command'} ${notice.confidence === 'likely' ? 'may be waiting for input' : 'is waiting for input'} · ${age} · /resume`}; case 'long-running': return {kind: notice.kind, symbol: 'long', text: `${label} · ${who ?? 'command'} still running · ${formatDuration(notice.durationMs ?? 0)}`}; case 'ended': diff --git a/src/session/SessionProtocol.ts b/src/session/SessionProtocol.ts index eac4b178..f4697066 100644 --- a/src/session/SessionProtocol.ts +++ b/src/session/SessionProtocol.ts @@ -10,6 +10,7 @@ */ import {NOTICE_KINDS, type SessionNotice} from './SessionNotices.js'; +import {INPUT_CONFIDENCES, INPUT_MODES, type InputState} from './inputState.js'; // v2: a frontend going away detaches its session instead of ending it. export const PROTOCOL_VERSION = 2; @@ -21,15 +22,28 @@ export const MAX_FRAME_BYTES = 8 * 1024 * 1024; * talking to an older service (same protocol version, still running its live * sessions) degrades factually instead of failing later. */ -export const SERVICE_FEATURES = ['shell-switch', 'shell-backends', 'notices'] as const; +export const SERVICE_FEATURES = ['shell-switch', 'shell-backends', 'notices', 'input-state'] as const; export type ServiceFeature = typeof SERVICE_FEATURES[number]; +/** + * What a frontend understands beyond the base protocol, sent in its hello. A service sends a feature's messages or + * values (the input-state message, the "input" notice kind) only to a frontend that listed it, so an older frontend + * attached to a newer service never receives a frame it would reject. + */ +export const CLIENT_FEATURES = ['input-state'] as const; +export type ClientFeature = typeof CLIENT_FEATURES[number]; + +export function parseClientFeatures(text: string | undefined): Set { + return new Set((text ?? '').split(',').map(item => item.trim()).filter((item): item is ClientFeature => (CLIENT_FEATURES as readonly string[]).includes(item))); +} + export function parseFeatures(text: string | undefined): Set { return new Set((text ?? '').split(',').map(item => item.trim()).filter((item): item is ServiceFeature => (SERVICE_FEATURES as readonly string[]).includes(item))); } export type ClientMessage = - | {type: 'hello'; version: number; client: string} + /** features: comma-separated CLIENT_FEATURES this frontend understands; absent from older frontends. */ + | {type: 'hello'; version: number; client: string; features?: string} /** shell: backend id (zsh, fish, bash); absent means zsh (older frontends). */ | {type: 'create'; cwd: string; env: Record; columns: number; rows: number; shell?: string} | {type: 'attach'; sessionId: string; columns: number; rows: number} @@ -80,7 +94,12 @@ export type ServerMessage = */ | {type: 'output'; data: string; seq?: number; at?: number} | {type: 'exec'; command: string; seq: number; at: number; historyAllowed?: number} - | {type: 'prompt'; exitCode: number; cwd: string; knowledge?: string; seq?: number; at?: number} + | {type: 'prompt'; exitCode: number; cwd: string; knowledge?: string; seq?: number; at?: number; inputWaitMs?: number; inputWaits?: number} + /** + * The running command's input state (see InputWatch), sent on every change to frontends that support it: a + * request (since/confidence/mode, with the open prompt and program when known) or none, plus waiting time so far. + */ + | {type: 'input-state'; since?: number; confidence?: string; mode?: string; prompt?: string; program?: string; waitedMs: number; waits: number} /** End of the backlog sent after attach. */ | {type: 'replayed'; truncatedBytes: number} | {type: 'killed'; sessionId: string} @@ -124,6 +143,11 @@ export interface SessionInfo { name?: string; /** The session's current cross-session notice, until it is focused (#v0.16; absent from older services). */ notice?: SessionNotice; + /** While the running command credibly waits for input: since when, how sure, how it reads, and the prompt shown. */ + inputSince?: number; + inputConfidence?: string; + inputMode?: string; + inputPrompt?: string; } export type ProtocolMessage = ClientMessage | ServerMessage; @@ -140,7 +164,7 @@ type Kind = 'string' | 'int' | 'env' | 'int?' | 'string?' | 'sessions' | 'notice type Shape = Record; const SHAPES: Record = { - hello: {version: 'int', client: 'string'}, + hello: {version: 'int', client: 'string', features: 'string?'}, create: {cwd: 'string', env: 'env', columns: 'int', rows: 'int', shell: 'string?'}, 'switch-shell': {shell: 'string', cwd: 'string'}, rename: {sessionId: 'string', name: 'string'}, @@ -166,7 +190,8 @@ const SHAPES: Record = { dismissed: {sessionId: 'string'}, output: {data: 'string', seq: 'int?', at: 'int?'}, exec: {command: 'string', seq: 'int', at: 'int', historyAllowed: 'int?'}, - prompt: {exitCode: 'int', cwd: 'string', knowledge: 'string?', seq: 'int?', at: 'int?'}, + prompt: {exitCode: 'int', cwd: 'string', knowledge: 'string?', seq: 'int?', at: 'int?', inputWaitMs: 'int?', inputWaits: 'int?'}, + 'input-state': {since: 'int?', confidence: 'string?', mode: 'string?', prompt: 'string?', program: 'string?', waitedMs: 'int', waits: 'int'}, replayed: {truncatedBytes: 'int'}, killed: {sessionId: 'string'}, exit: {exitCode: 'int', signal: 'int?'}, @@ -179,10 +204,11 @@ function isEnv(value: unknown): value is Record { const INFO_SHAPE: Shape = {id: 'string', pid: 'int', state: 'string', cwd: 'string', createdAt: 'int', running: 'string?', runningSince: 'int?', idleSince: 'int?', journalId: 'string?', - process: 'string?', fullscreen: 'int?', lastOutputAt: 'int?', title: 'string?', attentionSince: 'int?', lastExit: 'int?', notice: 'notice?', shell: 'string?', signature: 'string?', name: 'string?'}; + process: 'string?', fullscreen: 'int?', lastOutputAt: 'int?', title: 'string?', attentionSince: 'int?', lastExit: 'int?', notice: 'notice?', shell: 'string?', signature: 'string?', name: 'string?', + inputSince: 'int?', inputConfidence: 'string?', inputMode: 'string?', inputPrompt: 'string?'}; const NOTICE_SHAPE: Shape = {sessionId: 'string', kind: 'string', at: 'int', program: 'string?', agent: 'string?', exitCode: 'int?', - durationMs: 'int?', cwd: 'string?'}; + durationMs: 'int?', cwd: 'string?', confidence: 'string?'}; function decodeNotice(raw: unknown): SessionNotice | undefined { const decoded = decodeShape(NOTICE_SHAPE, raw) as SessionNotice | undefined; @@ -257,3 +283,20 @@ export class FrameDecoder { return results; } } + +/** The input-state message for a state; bounded prompt text. */ +export function inputStateMessage(state: InputState): Extract { + const request = state.request; + return {type: 'input-state', waitedMs: Math.round(state.timing.waitedMs), waits: state.timing.waits, + ...(request ? {since: request.since, confidence: request.confidence, mode: request.mode, + ...(request.prompt ? {prompt: request.prompt.slice(0, 200)} : {}), ...(request.program ? {program: request.program.slice(0, 64)} : {})} : {})}; +} + +/** The state an input-state message describes; an unknown confidence or mode is dropped, never guessed. */ +export function inputStateFrom(message: Extract): InputState { + const confidence = INPUT_CONFIDENCES.find(item => item === message.confidence); + const mode = INPUT_MODES.find(item => item === message.mode); + const timing = {waitedMs: message.waitedMs, waits: message.waits}; + if (message.since === undefined || !confidence || !mode) return {timing}; + return {request: {since: message.since, confidence, mode, ...(message.prompt ? {prompt: message.prompt} : {}), ...(message.program ? {program: message.program} : {})}, timing}; +} diff --git a/src/session/SessionService.ts b/src/session/SessionService.ts index bef57336..24ed410a 100644 --- a/src/session/SessionService.ts +++ b/src/session/SessionService.ts @@ -6,10 +6,12 @@ import {ShellSession} from '../shell/ShellSession.js'; import {isShellId, knowledgeJobCount, type ShellId} from '../shell/adapters/ShellAdapter.js'; import {shellAdapter} from '../shell/adapters/registry.js'; import {SessionEvidence} from './SessionEvidence.js'; -import {FrameDecoder, PROTOCOL_VERSION, SERVICE_FEATURES, encodeMessage, type ServerMessage, type SessionInfo, type SessionState} from './SessionProtocol.js'; +import {FrameDecoder, PROTOCOL_VERSION, SERVICE_FEATURES, encodeMessage, inputStateMessage, parseClientFeatures, type ClientFeature, type ServerMessage, + type SessionInfo, type SessionState} from './SessionProtocol.js'; +import type {InputState} from './inputState.js'; import {SESSION_ID_ENV, SESSION_MODE_ENV} from './SessionClient.js'; import {ensurePrivateRuntimeDir, socketPathFor, spoolPathFor} from './runtimeDir.js'; -import {EndedNotices, SessionNoticeTracker} from './SessionNotices.js'; +import {EndedNotices, INPUT_NOTICE_DELAY_MS, SessionNoticeTracker} from './SessionNotices.js'; import {StreamBacklog, type BacklogEvent, type BacklogLimits} from './StreamBacklog.js'; export const SERVICE_NAME = 'nmshd'; @@ -39,6 +41,12 @@ interface ManagedSession { size: {columns: number; rows: number}; /** The one writable frontend; undefined while detached. */ controller?: Send; + /** What the controlling frontend said it understands (its hello). */ + controllerFeatures?: ReadonlySet; + /** The running command's input state (InputWatch, beside the PTY): the one source every surface reads. */ + input?: InputState; + /** Pending check that a wait lasted long enough to become a cross-session notice. */ + inputNoticeTimer?: NodeJS.Timeout; running?: {command: string; since: number}; /** When zsh last returned to its prompt. */ idleSince: number; @@ -79,7 +87,8 @@ function toMessage(event: BacklogEvent): ServerMessage { case 'exec': return {type: 'exec', command: event.command, seq: event.seq, at: event.at, ...(event.historyAllowed === undefined ? {} : {historyAllowed: event.historyAllowed})}; case 'prompt': return {type: 'prompt', exitCode: event.exitCode, cwd: event.cwd, seq: event.seq, at: event.at, - ...(event.knowledge === undefined ? {} : {knowledge: event.knowledge})}; + ...(event.knowledge === undefined ? {} : {knowledge: event.knowledge}), + ...(event.inputWaits ? {inputWaitMs: event.inputWaitMs ?? 0, inputWaits: event.inputWaits} : {})}; } } @@ -120,15 +129,17 @@ export class SessionService { return [...this.sessions.values()].map(session => ({...session.record})); } - private info(session: ManagedSession): SessionInfo { + private info(session: ManagedSession, features: ReadonlySet = new Set()): SessionInfo { const {record, running} = session; + const request = running ? session.input?.request : undefined; const evidence = session.evidence.snapshot(); // Read only when someone lists sessions; nothing polls the process table. const process = running ? session.shell.foregroundProcess : undefined; session.notices.observeProcess(process); if (evidence.attentionSince !== undefined) session.notices.onAttention(evidence.attentionSince); session.notices.checkLongRunning(Date.now()); - const notice = session.notices.notice; + // A notice kind an older frontend cannot decode is left out for it (it would reject the whole list). + const notice = session.notices.notice?.kind === 'input' && !features.has('input-state') ? undefined : session.notices.notice; return {id: record.id, pid: record.pid, state: record.state, cwd: record.cwd, createdAt: Date.parse(record.createdAt), shell: session.backend, ...(record.signature ? {signature: record.signature} : {}), ...(record.name ? {name: record.name} : {}), ...(running ? {running: running.command, runningSince: running.since} : {idleSince: session.idleSince}), @@ -139,7 +150,9 @@ export class SessionService { ...(evidence.title ? {title: evidence.title} : {}), ...(evidence.attentionSince !== undefined ? {attentionSince: evidence.attentionSince} : {}), ...(evidence.lastExit !== undefined && !running ? {lastExit: evidence.lastExit} : {}), - ...(notice ? {notice} : {})}; + ...(notice ? {notice} : {}), + ...(request ? {inputSince: request.since, inputConfidence: request.confidence, inputMode: request.mode, + ...(request.prompt ? {inputPrompt: request.prompt.slice(0, 120)} : {})} : {})}; } async start(): Promise { @@ -174,6 +187,7 @@ export class SessionService { const send = (message: ServerMessage) => { if (!socket.destroyed) socket.write(encodeMessage(message)); }; let greeted = false; let owned: ManagedSession | undefined; + let features: ReadonlySet = new Set(); socket.on('data', chunk => { for (const result of decoder.push(chunk as unknown as string)) { @@ -191,6 +205,7 @@ export class SessionService { return; } greeted = true; + features = parseClientFeatures(message.features); send({type: 'welcome', version: PROTOCOL_VERSION, service: SERVICE_NAME, startupSafety: 1, features: SERVICE_FEATURES.join(','), ...(this.options.build ? {build: this.options.build} : {})}); continue; @@ -206,6 +221,7 @@ export class SessionService { const backend = message.shell === undefined ? 'zsh' : message.shell; if (!isShellId(backend)) throw new Error(`unknown shell backend ${backend}`); owned = this.create(message.cwd, message.env, message.columns, message.rows, send, backend); + owned.controllerFeatures = features; send({type: 'created', sessionId: owned.record.id, pid: owned.record.pid, shell: owned.backend}); } catch (error) { send({type: 'error', code: 'spawn', message: error instanceof Error ? error.message : String(error)}); @@ -217,11 +233,11 @@ export class SessionService { if (!session) { send({type: 'error', code: 'unknown', message: 'no live session with that id'}); break; } if (session.controller) { send({type: 'error', code: 'attached', message: 'session is attached to another frontend'}); break; } owned = session; - this.bind(session, send); + this.bind(session, send, features); // Opening a session is focusing it: its notice is no longer news in any window. session.notices.clear(); this.endedNotices.dismiss(session.record.id); - const info = this.info(session); + const info = this.info(session, features); const {backlog} = session; send({type: 'attached', sessionId: info.id, pid: info.pid, cwd: info.cwd, fullscreen: session.screen.ownsTerminal ? 1 : 0, ...(session.screen.ownsTerminal && session.screen.restoreSequence() ? {modes: session.screen.restoreSequence()} : {}), @@ -233,6 +249,8 @@ export class SessionService { const missed = backlog.events(); for (const event of missed) send(toMessage(event)); send({type: 'replayed', truncatedBytes: backlog.truncatedBytes}); + // The live input state is not a stream event: a reattaching frontend gets it now, after the replay. + if (features.has('input-state') && info.running && session.input) send(inputStateMessage(session.input)); this.redraw(session, message.columns, message.rows); break; } @@ -245,7 +263,7 @@ export class SessionService { } break; case 'list': - send({type: 'sessions', sessions: [...this.sessions.values()].map(session => this.info(session)), ended: this.endedNotices.list(Date.now())}); + send({type: 'sessions', sessions: [...this.sessions.values()].map(session => this.info(session, features)), ended: this.endedNotices.list(Date.now())}); break; case 'dismiss': this.sessions.get(message.sessionId)?.notices.clear(); @@ -279,7 +297,7 @@ export class SessionService { if (!target) { send({type: 'error', code: 'unknown', message: 'no live session with that id'}); break; } const name = message.name.replace(/[\u0000-\u001f\u007f]/gu, '').trim().slice(0, 40); if (name) target.record.name = name; else delete target.record.name; - send({type: 'sessions', sessions: [...this.sessions.values()].map(session => this.info(session)), ended: this.endedNotices.list(Date.now())}); + send({type: 'sessions', sessions: [...this.sessions.values()].map(session => this.info(session, features)), ended: this.endedNotices.list(Date.now())}); break; } case 'resize': @@ -309,17 +327,43 @@ export class SessionService { }); } - private bind(session: ManagedSession, send: Send): void { + private bind(session: ManagedSession, send: Send, features: ReadonlySet): void { session.controller = send; + session.controllerFeatures = features; session.record.state = 'attached'; } private detach(session: ManagedSession, send: Send): void { if (session.controller !== send) return; session.controller = undefined; + session.controllerFeatures = undefined; session.record.state = 'detached'; } + /** + * A new input state from the session's watch: the attached frontend hears it at once; a wait that lasts becomes + * one cross-session notice (sticky until answered or focused), and a resolved wait withdraws it. + */ + private onInputState(session: ManagedSession, state: InputState): void { + const before = session.input?.request; + session.input = state; + if (session.controllerFeatures?.has('input-state')) session.controller?.(inputStateMessage(state)); + const request = state.request; + if (!request) { + if (session.inputNoticeTimer) { clearTimeout(session.inputNoticeTimer); session.inputNoticeTimer = undefined; } + if (before) session.notices.onInputResolved(); + return; + } + if (before?.since === request.since && session.inputNoticeTimer) return; + if (session.inputNoticeTimer) clearTimeout(session.inputNoticeTimer); + session.inputNoticeTimer = setTimeout(() => { + session.inputNoticeTimer = undefined; + const current = session.input?.request; + if (current && current.since === request.since) session.notices.onInputNeeded(current.since, current.confidence, current.program); + }, Math.max(0, request.since + INPUT_NOTICE_DELAY_MS - Date.now())); + session.inputNoticeTimer.unref?.(); + } + /** * Adopt the attaching frontend's size. A fullscreen app only repaints on a * real size change, so step through a neighbouring size first: each step @@ -380,6 +424,7 @@ export class SessionService { else session.controller?.({type: 'output', data}); }); shell.on('startup', output => session.controller?.({type: 'startup', output})); + shell.on('inputState', state => this.onInputState(session, state)); shell.on('exec', (command, historyAllowed) => { const at = Date.now(); session.running = {command, since: at}; @@ -396,10 +441,12 @@ export class SessionService { session.notices.onPrompt(marker.exitCode, Date.now(), marker.cwd); session.screen.reset(); emit({kind: 'prompt', seq: ++session.seq, at: Date.now(), exitCode: marker.exitCode, cwd: marker.cwd, - ...(marker.knowledge === undefined ? {} : {knowledge: marker.knowledge})}); + ...(marker.knowledge === undefined ? {} : {knowledge: marker.knowledge}), + ...(marker.inputWaits ? {inputWaitMs: marker.inputWaitMs ?? 0, inputWaits: marker.inputWaits} : {})}); }); shell.on('exit', event => { this.sessions.delete(record.id); + if (session.inputNoticeTimer) { clearTimeout(session.inputNoticeTimer); session.inputNoticeTimer = undefined; } // Ending with no window attached is news; with one attached, that window saw it. if (!session.controller) this.endedNotices.add(session.notices.ended(event.exitCode, Date.now(), record.cwd)); // Detached: keep what the journal lacks on disk for archiving. Attached: diff --git a/src/session/SocketSessionClient.ts b/src/session/SocketSessionClient.ts index 810d4b20..3e13f8d3 100644 --- a/src/session/SocketSessionClient.ts +++ b/src/session/SocketSessionClient.ts @@ -1,6 +1,6 @@ import {EventEmitter} from 'node:events'; import {connect, type Socket} from 'node:net'; -import {FrameDecoder, PROTOCOL_VERSION, encodeMessage, parseFeatures, type ClientMessage, type ServiceFeature, type ServerMessage} from './SessionProtocol.js'; +import {CLIENT_FEATURES, FrameDecoder, PROTOCOL_VERSION, encodeMessage, inputStateFrom, parseFeatures, type ClientMessage, type ServiceFeature, type ServerMessage} from './SessionProtocol.js'; import type {SessionInfo} from './SessionProtocol.js'; import type {SessionNotice} from './SessionNotices.js'; import {isShellId, type ShellId} from '../shell/adapters/ShellAdapter.js'; @@ -78,7 +78,7 @@ function request(socketPath: string, timeoutMs: number, first: ClientMessage }; const onError = (error: NodeJS.ErrnoException) => fail(`session service unreachable (${error.code ?? 'error'})`, error.code ?? 'error'); const onClose = () => fail('session service closed the connection', 'closed'); - socket.once('connect', () => send({type: 'hello', version: PROTOCOL_VERSION, client: 'nmsh'})); + socket.once('connect', () => send({type: 'hello', version: PROTOCOL_VERSION, client: 'nmsh', features: CLIENT_FEATURES.join(',')})); socket.on('error', onError); socket.on('close', onClose); socket.on('data', onData); @@ -233,7 +233,9 @@ export class SocketSessionClient extends EventEmitter imple } if (message.type === 'output') this.emit('data', message.data, {seq: message.seq, at: message.at}); else if (message.type === 'prompt') this.emit('prompt', {exitCode: message.exitCode, cwd: message.cwd, - ...(message.knowledge === undefined ? {} : {knowledge: message.knowledge})}, {seq: message.seq, at: message.at}); + ...(message.knowledge === undefined ? {} : {knowledge: message.knowledge}), + ...(message.inputWaits ? {inputWaitMs: message.inputWaitMs ?? 0, inputWaits: message.inputWaits} : {})}, {seq: message.seq, at: message.at}); + else if (message.type === 'input-state') this.emit('inputState', inputStateFrom(message)); else if (message.type === 'exec') this.emit('exec', message.command, {seq: message.seq, at: message.at, historyAllowed: message.historyAllowed}); else if (message.type === 'input-rejected') this.emit('inputRejected', message.data, message.submission === 1); else if (message.type === 'startup') this.emit('startup', message.output); diff --git a/src/session/StreamBacklog.ts b/src/session/StreamBacklog.ts index 6d4c6e1d..d936184d 100644 --- a/src/session/StreamBacklog.ts +++ b/src/session/StreamBacklog.ts @@ -6,7 +6,7 @@ import {MAX_SHELL_KNOWLEDGE_BYTES} from '../shell/ShellKnowledge.js'; export type BacklogEvent = | {kind: 'output'; seq: number; at: number; data: string} | {kind: 'exec'; seq: number; at: number; command: string; historyAllowed?: number} - | {kind: 'prompt'; seq: number; at: number; exitCode: number; cwd: string; knowledge?: string}; + | {kind: 'prompt'; seq: number; at: number; exitCode: number; cwd: string; knowledge?: string; inputWaitMs?: number; inputWaits?: number}; /** Non-event spool records: journal acknowledgements, truncation and the shell's end. */ type SpoolRecord = BacklogEvent diff --git a/src/session/inputState.ts b/src/session/inputState.ts new file mode 100644 index 00000000..bfebc93b --- /dev/null +++ b/src/session/inputState.ts @@ -0,0 +1,51 @@ +/** + * The one shared description of "a command needs the person": produced next to the PTY (InputWatch), carried by the + * session protocol, and only rendered by frontends. No surface guesses on its own. + */ + +/** How the foreground program reads: a line until Enter, a hidden line (password), or single keys. */ +export type InputMode = 'line' | 'hidden' | 'key'; +export const INPUT_MODES: readonly InputMode[] = ['line', 'hidden', 'key']; + +/** + * confirmed: the terminal or the kernel says a program is reading input (hidden or key input set on the terminal + * with a prompt left on screen, or a blocked read of the terminal). likely: a question-shaped line was left open, + * the output is quiet and the foreground processes are idle, but nothing proves they are reading. + */ +export type InputConfidence = 'confirmed' | 'likely'; +export const INPUT_CONFIDENCES: readonly InputConfidence[] = ['confirmed', 'likely']; + +export interface InputRequest { + /** When the program started waiting: when it wrote the prompt it is waiting on (epoch ms). */ + since: number; + confidence: InputConfidence; + mode: InputMode; + /** The open prompt line, sanitized and bounded; absent when the program left none. */ + prompt?: string; + /** Foreground program name, when the platform reports it. */ + program?: string; +} + +/** Waiting time of the current (or just finished) command; only intervals whose boundaries NMSh saw. */ +export interface InputTiming { + waitedMs: number; + waits: number; +} + +export interface InputState { + request?: InputRequest; + timing: InputTiming; +} + +export const NO_INPUT_TIMING: InputTiming = Object.freeze({waitedMs: 0, waits: 0}); + +/** Waiting time including an open wait up to now. */ +export function waitedSoFar(state: InputState | undefined, now: number): number { + if (!state) return 0; + return state.timing.waitedMs + (state.request ? Math.max(0, now - state.request.since) : 0); +} + +/** Keys go to the program one by one (hidden line or key input) instead of through the composer's line editor. */ +export function directInput(request: InputRequest | undefined): boolean { + return request?.confidence === 'confirmed' && (request.mode === 'hidden' || request.mode === 'key'); +} diff --git a/src/session/liveStatus.ts b/src/session/liveStatus.ts index 37716dba..8cf6448a 100644 --- a/src/session/liveStatus.ts +++ b/src/session/liveStatus.ts @@ -33,8 +33,8 @@ export function knownProgram(session: SessionInfo): string | undefined { /** * Factual status from service evidence. Every clause is something the session * reported: what runs, whether it wrote recently, whether it asked for - * attention, the title it set, or how the last command ended. Nothing is - * inferred about what a program is waiting for; unknown stays unsaid. + * attention, the title it set, whether its input watch saw it wait for + * input, or how the last command ended. Unknown stays unsaid. */ export function liveStatusParts(session: SessionInfo, now: number): string[] { if (!session.running) { @@ -46,7 +46,11 @@ export function liveStatusParts(session: SessionInfo, now: number): string[] { const known = knownProgram(session); if (known) parts.push(known); else if (session.process && session.process !== commandWord(session.running)) parts.push(`process ${session.process}`); - if (session.attentionSince !== undefined) parts.push(`needs attention ${formatAge(now - session.attentionSince)}`); + // The service's input watch is the one source: confirmed or likely, never re-guessed here. + if (session.inputSince !== undefined) { + parts.push(`${session.inputConfidence === 'likely' ? 'probably waiting for input' : 'waiting for input'} ${formatAge(Math.max(0, now - session.inputSince))}`); + if (session.inputPrompt) parts.push(`“${session.inputPrompt.replace(/\s+/gu, ' ').slice(0, 40)}”`); + } else if (session.attentionSince !== undefined) parts.push(`needs attention ${formatAge(now - session.attentionSince)}`); else if (session.lastOutputAt !== undefined) { const quietFor = now - session.lastOutputAt; parts.push(quietFor < ACTIVE_OUTPUT_MS ? 'active' : `quiet ${formatAge(quietFor)}`); diff --git a/src/session/openLine.ts b/src/session/openLine.ts new file mode 100644 index 00000000..c569f501 --- /dev/null +++ b/src/session/openLine.ts @@ -0,0 +1,91 @@ +/** + * The line a running program left the cursor on, as far as its output stream shows it: the evidence a prompt + * leaves behind ("Fix? ", "Password: ", "Continue? [Y/n] "). Only plain text written left to right counts; + * a line the program rewrote (carriage-return progress) or drew with cursor addressing is marked unreliable, so + * a stalled progress bar or a redrawn status line is never mistaken for a question. + */ +const MAX_LINE = 240; +// CSI parameters and intermediates, then the final byte. +const CSI = /^\u001b\[[0-?]*[ -/]*([@-~])/u; +// OSC/DCS/APC/PM/SOS payloads end in BEL or ST. +const STRING = /^\u001b[\]P_^X][\s\S]*?(?:\u0007|\u001b\\)/u; + +export class OpenLine { + private text = ''; + private rewrites = 0; + private addressed = false; + private pendingReturn = false; + private carry = ''; + + push(data: string): void { + const input = this.carry + data; + this.carry = ''; + let index = 0; + while (index < input.length) { + const char = input[index]!; + if (this.pendingReturn) { + this.pendingReturn = false; + if (char !== '\n') { this.text = ''; this.rewrites += 1; } + } + if (char === '\u001b') { + const rest = input.slice(index); + const csi = CSI.exec(rest); + if (csi) { + // Cursor addressing (moves, column/row positioning) means the line is drawn, not written. + if (/[A-Hdf`]/u.test(csi[1]!)) this.addressed = true; + index += csi[0].length; + continue; + } + const string = STRING.exec(rest); + if (string) { index += string[0].length; continue; } + // Possibly a sequence split across reads: keep a bounded tail for the next chunk. + if (rest.length < 64 && /^\u001b(?:\[[0-?]*[ -/]*|[\]P_^X][^\u0007]*)?$/u.test(rest)) { this.carry = rest; break; } + // ESC + one byte (charset, keypad mode): no text. + index += Math.min(2, rest.length); + continue; + } + if (char === '\n') { this.text = ''; this.rewrites = 0; this.addressed = false; } + else if (char === '\r') this.pendingReturn = true; + else if (char === '\b') this.text = [...this.text].slice(0, -1).join(''); + else if (char === '\t') this.text += ' '; + else if (char >= ' ' && char !== '\u007f') this.text += char; + index += 1; + } + if (this.text.length > MAX_LINE) this.text = this.text.slice(-MAX_LINE); + } + + reset(): void { + this.text = ''; + this.rewrites = 0; + this.addressed = false; + this.pendingReturn = false; + this.carry = ''; + } + + /** The visible text left on the cursor's line (trailing spaces kept: they are part of a prompt). */ + get line(): string { return this.pendingReturn ? '' : this.text; } + + /** Rewritten in place or drawn with cursor addressing: not a line written once, so not prompt evidence. */ + get unreliable(): boolean { return this.rewrites > 0 || this.addressed; } + + /** Text was left on the line and nothing rewrote it. */ + get open(): boolean { return this.line.trim() !== '' && !this.unreliable; } +} + +/** + * The shape a question or entry prompt ends in: a question mark, a colon, or a closing bracket/angle that ends a + * choice list ("[Y/n]", "(yes/no)", "", ">>>"). A line ending in an ellipsis is a status ("Downloading..."), + * not a request. This is a shape test on the open line only; it never decides anything on its own. + */ +export function promptShaped(line: string): boolean { + const text = line.trimEnd(); + if (!text || /(?:\.\.\.|…)$/u.test(text)) return false; + return /[?:>\])»?:]$/u.test(text); +} + +/** The prompt as NMSh shows it: one line, control-free, bounded. */ +export function promptText(line: string, limit = 120): string | undefined { + const text = line.replace(/[\u0000-\u001f\u007f-\u009f]/gu, '').trim(); + if (!text) return undefined; + return text.length > limit ? `…${text.slice(-(limit - 1))}` : text; +} diff --git a/src/session/terminalProbe.ts b/src/session/terminalProbe.ts new file mode 100644 index 00000000..3f60f500 --- /dev/null +++ b/src/session/terminalProbe.ts @@ -0,0 +1,168 @@ +import {execFile} from 'node:child_process'; +import {existsSync} from 'node:fs'; +import {readdir, readFile, readlink} from 'node:fs/promises'; +import {arch as hostArch} from 'node:os'; + +/** + * Read-only facts about a session's terminal and its foreground process group, gathered from the operating system + * while a command runs and its output has gone quiet. Nothing here writes to the terminal, signals a process or + * reads input: `stty -a` on the terminal device only reports its line settings, `ps` lists processes, and Linux's + * /proc reports what a process is blocked in. Every fact is optional; what the platform cannot tell stays unknown. + */ +export interface TerminalModes { + /** ICANON: the kernel collects a line until Enter. Off means the program reads keys as they come. */ + canonical: boolean; + /** ECHO: typed characters are shown. Off while a program reads a password or a single key. */ + echo: boolean; + /** ISIG: Ctrl+C and Ctrl+Z become signals. Off in full raw mode (terminal proxies, editors). */ + signals: boolean; +} + +export interface ForegroundProcess { + pid: number; + /** ps state letters; R is running on a CPU. */ + state: string; + /** Cumulative CPU time in milliseconds, when the platform reports it. */ + cpuMs?: number; + command: string; +} + +export interface TerminalSample { + modes?: TerminalModes; + foreground: ForegroundProcess[]; + /** + * The kernel's report (Linux): true when a foreground process is blocked in read() on this terminal; false when + * every foreground thread is blocked in something else (sleeping, waiting on a socket or a child); undefined + * when the platform cannot tell or a thread is waiting in poll/select, whose descriptors are not visible. + */ + readingTerminal?: boolean; +} + +export interface ProbeTarget { + /** The terminal device, e.g. /dev/ttys015 or /dev/pts/3. */ + tty: string; +} + +export type TerminalProbe = (target: ProbeTarget) => Promise; + +const TIMEOUT_MS = 1500; + +function run(file: string, args: string[]): Promise { + return new Promise(resolve => { + execFile(file, args, {timeout: TIMEOUT_MS, maxBuffer: 256 * 1024, encoding: 'utf8', env: {LC_ALL: 'C', PATH: '/usr/bin:/bin'}}, + (error, stdout) => resolve(error ? undefined : stdout)); + }); +} + +/** Line settings from `stty -a` (BSD and GNU layouts): flags are whole words, `-flag` when off. */ +export function parseStty(text: string): TerminalModes | undefined { + const words = new Set(text.split(/[\s;]+/u)); + const flag = (name: string) => words.has(name) ? true : words.has(`-${name}`) ? false : undefined; + const canonical = flag('icanon'), echo = flag('echo'), signals = flag('isig'); + if (canonical === undefined || echo === undefined) return undefined; + return {canonical, echo, signals: signals ?? true}; +} + +/** CPU time as ps prints it: [[dd-]hh:]mm:ss[.cc]. */ +export function parseCpuTime(text: string): number | undefined { + const match = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)(?:\.(\d+))?$/u.exec(text.trim()); + if (!match) return undefined; + const [, days = '0', hours = '0', minutes, seconds, fraction = ''] = match; + return ((Number(days) * 24 + Number(hours)) * 60 + Number(minutes)) * 60_000 + Number(seconds) * 1000 + Number(`0.${fraction || '0'}`) * 1000; +} + +/** + * The foreground process group of the terminal from `ps -o pid=,pgid=,tpgid=,stat=,time=,comm= -t TTY`: the + * processes whose group is the terminal's foreground group (pgid equals tpgid). + */ +export function parseForeground(text: string): ForegroundProcess[] { + const rows: ForegroundProcess[] = []; + for (const line of text.split('\n')) { + const match = /^\s*(\d+)\s+(\d+)\s+(-?\d+)\s+(\S+)\s+(\S+)\s+(.*)$/u.exec(line); + if (!match || match[2] !== match[3]) continue; + const cpuMs = parseCpuTime(match[5]!); + rows.push({pid: Number(match[1]), state: match[4]!, ...(cpuMs === undefined ? {} : {cpuMs}), command: match[6]!.trim().split('/').pop() ?? ''}); + } + return rows.slice(0, 64); +} + +/** Syscall numbers of read(2)-family calls and of poll/select-family waits, per Linux architecture. */ +const LINUX_SYSCALLS: Record = { + x64: {read: [0, 17, 19, 295, 327], wait: [7, 23, 232, 270, 271, 281, 441]}, + arm64: {read: [63, 65, 67, 69, 286], wait: [22, 72, 73, 441]}, +}; + +/** + * One line of /proc/PID/task/TID/syscall: "NR ARG0 ... SP PC" while blocked in a call, "running" or "-1 ..." + * otherwise. Returns what the thread waits on: a read of descriptor fd, a poll/select wait, something else, or + * undefined while it runs. + */ +export function parseSyscall(text: string, arch = hostArch()): {kind: 'read'; fd: number} | {kind: 'wait' | 'other'} | undefined { + const fields = text.trim().split(/\s+/u); + const nr = Number(fields[0]); + if (!Number.isInteger(nr) || nr < 0) return undefined; + const table = LINUX_SYSCALLS[arch]; + if (!table) return {kind: 'other'}; + if (table.read.includes(nr)) { + const fd = Number.parseInt(fields[1] ?? '', 16); + return Number.isInteger(fd) ? {kind: 'read', fd} : {kind: 'other'}; + } + return table.wait.includes(nr) ? {kind: 'wait'} : {kind: 'other'}; +} + +/** Linux: whether the foreground group is blocked reading this terminal, from the kernel's per-thread report. */ +async function linuxReading(pids: number[], tty: string): Promise { + let unknown = false; + for (const pid of pids.slice(0, 16)) { + let threads: string[]; + try { threads = (await readdir(`/proc/${pid}/task`)).slice(0, 64); } catch { return undefined; } + for (const tid of threads) { + let text: string; + // Readable for the shell's own descendants; a privileged (setuid) program refuses, which stays unknown. + try { text = await readFile(`/proc/${pid}/task/${tid}/syscall`, 'utf8'); } catch { return undefined; } + const call = parseSyscall(text); + if (!call) { unknown = true; continue; } + if (call.kind === 'wait') { unknown = true; continue; } + if (call.kind !== 'read') continue; + let target: string; + try { target = await readlink(`/proc/${pid}/fd/${call.fd}`); } catch { unknown = true; continue; } + if (target === tty || target === '/dev/tty') return true; + } + } + return unknown ? undefined : false; +} + +async function linuxCpu(pid: number): Promise { + try { + const stat = await readFile(`/proc/${pid}/stat`, 'utf8'); + // Fields after the parenthesised command; utime and stime are the 12th and 13th of them, in clock ticks (100 Hz). + const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' '); + return (Number(fields[11]) + Number(fields[12])) * 10; + } catch { return undefined; } +} + +const STTY = ['/bin/stty', '/usr/bin/stty'].find(existsSync); +const PS = ['/bin/ps', '/usr/bin/ps'].find(existsSync); + +/** The probe for this platform, or undefined where NMSh has no read-only way to look (Windows, missing tools). */ +export function systemProbe(platform: NodeJS.Platform = process.platform): TerminalProbe | undefined { + if ((platform !== 'darwin' && platform !== 'linux') || !STTY || !PS) return undefined; + const linux = platform === 'linux'; + return async ({tty}) => { + const name = tty.replace(/^\/dev\//u, ''); + const [stty, ps] = await Promise.all([run(STTY, [linux ? '-F' : '-f', tty, '-a']), run(PS, ['-o', 'pid=,pgid=,tpgid=,stat=,time=,comm=', '-t', name])]); + if (stty === undefined && ps === undefined) return undefined; + let foreground = ps === undefined ? [] : parseForeground(ps); + let readingTerminal: boolean | undefined; + if (linux && foreground.length) { + // ps prints whole seconds on Linux; the kernel's tick counts are precise enough to see a busy process. + foreground = await Promise.all(foreground.map(async item => { + const cpuMs = await linuxCpu(item.pid); + return cpuMs === undefined ? item : {...item, cpuMs}; + })); + readingTerminal = await linuxReading(foreground.map(item => item.pid), tty); + } + const modes = stty === undefined ? undefined : parseStty(stty); + return {...(modes ? {modes} : {}), foreground, ...(readingTerminal === undefined ? {} : {readingTerminal})}; + }; +} diff --git a/src/sessions/LiveSessionView.ts b/src/sessions/LiveSessionView.ts index 99789bc5..d2c8182b 100644 --- a/src/sessions/LiveSessionView.ts +++ b/src/sessions/LiveSessionView.ts @@ -27,7 +27,9 @@ export function liveSessionRows(sessions: readonly SessionInfo[], currentId: str return [...sessions].sort((a, b) => a.createdAt - b.createdAt).map((session, index) => { const state = liveRowState(session, now); const agent = liveRowAgent(session); - const what = session.running ? `running ${sessionText(session.running.replace(/\s+/gu, ' ')).slice(0, 48)} · ${formatAge(now - (session.runningSince ?? now))}` + // The question comes first: it is what the person needs, and long command lines are cut at the row's end. + const prompt = session.running && session.inputPrompt ? `“${sessionText(session.inputPrompt).slice(0, 48)}” · ` : ''; + const what = session.running ? `${prompt}running ${sessionText(session.running.replace(/\s+/gu, ' ')).slice(0, 48)} · ${formatAge(now - (session.runningSince ?? now))}` : `idle${session.idleSince ? ` ${formatAge(now - session.idleSince)}` : ''}`; const attachment = session.id === currentId ? 'this window' : session.state === 'attached' ? 'open in another window' : 'detached'; const notice = session.notice ? ` · notice: ${session.notice.kind}` : ''; diff --git a/src/sessions/ResumeBrowser.ts b/src/sessions/ResumeBrowser.ts index 52d7972a..28213f2e 100644 --- a/src/sessions/ResumeBrowser.ts +++ b/src/sessions/ResumeBrowser.ts @@ -116,14 +116,15 @@ export function resumeDayLabel(timestamp: string, now = new Date()): string { /** * The one-word state a live row leads with, strongest fact first. Only what - * the service reports: a pending attention request, recent output, a running - * command, or how the last command ended. "Awaiting input" is claimed only - * when the program itself asked for attention. + * the service reports: its input watch seeing the command wait for input + * (confirmed or likely, never re-guessed here), a pending attention request, + * recent output, a running command, or how the last command ended. */ -export type LiveRowState = 'attention' | 'active' | 'running' | 'completed' | 'failed' | 'idle'; +export type LiveRowState = 'input' | 'inputLikely' | 'attention' | 'active' | 'running' | 'completed' | 'failed' | 'idle'; export function liveRowState(session: SessionInfo, now: number): LiveRowState { if (session.running) { + if (session.inputSince !== undefined) return session.inputConfidence === 'likely' ? 'inputLikely' : 'input'; if (session.attentionSince !== undefined) return 'attention'; if (session.lastOutputAt !== undefined && now - session.lastOutputAt < ACTIVE_OUTPUT_MS) return 'active'; return 'running'; @@ -134,7 +135,7 @@ export function liveRowState(session: SessionInfo, now: number): LiveRowState { } export const LIVE_ROW_LABELS: Record = { - attention: 'Needs attention', active: 'Active', running: 'Running', completed: 'Completed', failed: 'Failed', idle: 'Idle', + input: 'Waiting for input', inputLikely: 'Probably waiting', attention: 'Needs attention', active: 'Active', running: 'Running', completed: 'Completed', failed: 'Failed', idle: 'Idle', }; /** A known agent, only when the command's program word or the foreground process proves it. */ @@ -146,10 +147,11 @@ export function liveRowAgent(session: SessionInfo): AgentDescriptor | undefined export function describeLiveRow(session: SessionInfo, now: number): string { const where = tildePath(session.cwd); const attached = session.state === 'attached' ? 'open in another window' : 'detached'; + const asked = session.running && session.inputPrompt ? `“${sessionText(session.inputPrompt).slice(0, 40)}” · ` : ''; const what = session.running - ? `${sessionText(session.running.replace(/\s+/gu, ' ')).slice(0, 48)} · ${formatAge(now - (session.runningSince ?? now))}` + ? `${asked}${sessionText(session.running.replace(/\s+/gu, ' ')).slice(0, 48)} · ${formatAge(now - (session.runningSince ?? now))}` : `idle${session.idleSince ? ` ${formatAge(now - session.idleSince)}` : ''}${session.lastExit !== undefined && session.lastExit !== 0 ? ` · last exit ${session.lastExit}` : ''}`; - const extra = session.title && session.running ? ` · “${sessionText(session.title).slice(0, 32)}”` : ''; + const extra = session.title && session.running && !session.inputPrompt ? ` · “${sessionText(session.title).slice(0, 32)}”` : ''; const name = session.name || session.signature; return `${name ? `${name} · ` : ''}${where} · ${what}${extra} · ${attached} · age ${formatAge(now - session.createdAt)}`; } diff --git a/src/sessions/TranscriptStore.ts b/src/sessions/TranscriptStore.ts index d915fb6a..ff39da30 100644 --- a/src/sessions/TranscriptStore.ts +++ b/src/sessions/TranscriptStore.ts @@ -85,6 +85,8 @@ function isTranscript(value: unknown): value is OutputTranscript { && transcript.records.every(record => record && (record.historyEligible === undefined || typeof record.historyEligible === 'boolean') && (record.startedAt === undefined || Number.isFinite(record.startedAt)) && (record.durationMs === undefined || (Number.isFinite(record.durationMs) && record.durationMs >= 0)) + && (record.inputWaitMs === undefined || (Number.isFinite(record.inputWaitMs) && record.inputWaitMs >= 0)) + && (record.inputWaits === undefined || (Number.isSafeInteger(record.inputWaits) && record.inputWaits >= 0)) && typeof record.command === 'string' && typeof record.output === 'string' && typeof record.lifecycleText === 'string' && typeof record.exitCode === 'number' && typeof record.startId === 'number' diff --git a/src/shell/ShellProtocol.ts b/src/shell/ShellProtocol.ts index a749f3b5..8d0a0a1f 100644 --- a/src/shell/ShellProtocol.ts +++ b/src/shell/ShellProtocol.ts @@ -2,6 +2,9 @@ export interface ShellMarker { exitCode: number; cwd: string; knowledge?: string; + /** Time the finished command spent waiting for input, and how many waits (see InputWatch); absent when none. */ + inputWaitMs?: number; + inputWaits?: number; } export type ProtocolEvent = diff --git a/src/shell/ShellSession.ts b/src/shell/ShellSession.ts index 159f61fd..c65c2545 100644 --- a/src/shell/ShellSession.ts +++ b/src/shell/ShellSession.ts @@ -11,6 +11,9 @@ import { basename, join } from 'node:path'; import {bridgeEnvPath} from '../themeBridge/environment.js'; import {PENDING_INPUT_LIMIT, sanitizeStartupOutput, STARTUP_RAW_LIMIT, utf8Tail} from './startupOutput.js'; import { ShellProtocolDecoder, type ShellMarker } from './ShellProtocol.js'; +import {InputWatch} from '../session/InputWatch.js'; +import type {InputState} from '../session/inputState.js'; +import {systemProbe, type TerminalProbe} from '../session/terminalProbe.js'; interface SessionEvents { data: [string]; @@ -20,6 +23,8 @@ interface SessionEvents { inputRejected: [string, boolean]; exec: [string, number?]; exit: [{ exitCode: number; signal?: number }]; + /** Whether the running command credibly waits for input, and its waiting time (see InputWatch). */ + inputState: [InputState]; } /** node-pty's error for an ioctl on a PTY whose descriptor is already closed. */ @@ -52,10 +57,14 @@ export class ShellSession extends EventEmitter { private readonly order: QueryOrder; /** The last composer submission, used when a shell cannot report a command's text (Bash, unrecorded lines). */ private lastSubmitted = ''; + /** One authoritative input-awareness state per session, next to the PTY. */ + readonly inputWatch: InputWatch; constructor(cwd: string, columns: number, rows: number, home = process.env.HOME || '', env: NodeJS.ProcessEnv = process.env, - shell: ShellId | ShellAdapter = 'zsh') { + shell: ShellId | ShellAdapter = 'zsh', probe: TerminalProbe | undefined = systemProbe()) { super(); + this.inputWatch = new InputWatch({probe, tty: () => this.ttyPath}); + this.inputWatch.on('change', state => this.emit('inputState', state)); this.adapter = typeof shell === 'string' ? shellAdapter(shell) : shell; this.order = new QueryOrder(data => { if (!this.exited) this.pty.write(data); }, QUERY_HOLD_MS, this.adapter.editorSettled); const token = randomBytes(12).toString('hex'); @@ -95,6 +104,7 @@ export class ShellSession extends EventEmitter { this.pty.onData(data => this.receive(data)); this.pty.onExit(event => { this.exited = true; + this.inputWatch.dispose(); this.pendingInput = ''; this.order.dispose(); if (this.startupTimer) { clearTimeout(this.startupTimer); this.startupTimer = undefined; } @@ -123,6 +133,12 @@ export class ShellSession extends EventEmitter { try { return this.pty.process || undefined; } catch { return undefined; } } + /** The PTY's terminal device (node-pty keeps it as a private field on Unix); undefined where unknown. */ + get ttyPath(): string | undefined { + const path = (this.pty as unknown as {_pty?: unknown})._pty; + return typeof path === 'string' && path.startsWith('/dev/') ? path : undefined; + } + /** Whether the first prompt has been reached. */ get isReady(): boolean { return this.ready; } @@ -152,7 +168,7 @@ export class ShellSession extends EventEmitter { */ private send(data: string, submission = false): void { if (submission) this.lastSubmitted = data.replace(/\r$/u, ''); - if (this.ready) { this.order.input(data); return; } + if (this.ready) { this.inputWatch.onInput(data); this.order.input(data); return; } if (this.pendingInputBytes() + Buffer.byteLength(data, 'utf8') <= PENDING_INPUT_LIMIT) this.pendingInput += data; else this.emit('inputRejected', data, submission); } @@ -161,11 +177,13 @@ export class ShellSession extends EventEmitter { // Input still held behind a terminal answer is discarded, as a terminal's Ctrl+C discards pending input: // nothing typed before the interrupt may reach the program after it. this.order.discard(); + this.inputWatch.onInput('\u0003'); if (!this.exited) this.pty.write('\u0003'); } endInput(): void { // End of input is ordinary input: it keeps its place after anything typed before it. + this.inputWatch.onInput('\u0004'); this.order.input('\u0004'); } @@ -191,6 +209,7 @@ export class ShellSession extends EventEmitter { this.exited = true; this.pendingInput = ''; this.order.dispose(); + this.inputWatch.dispose(); if (this.startupTimer) { clearTimeout(this.startupTimer); this.startupTimer = undefined; } try { this.pty.kill(); } finally { this.cleanup(); } } @@ -204,13 +223,17 @@ export class ShellSession extends EventEmitter { else if (this.adapter.editorChrome === 'prompt-to-exec' && this.atPrompt) this.answerChrome(event.data); else { const data = this.answerEditor(this.commandStart && this.adapter.scrubCommandStart ? this.adapter.scrubCommandStart(event.data) : event.data); - if (data) { this.commandStart = false; this.emit('data', data); } + if (data) { this.commandStart = false; this.inputWatch.onOutput(data); this.emit('data', data); } } } else if (event.kind === 'exec') { this.atPrompt = false; this.commandStart = true; // Bash cannot report the text of a line it chose not to record; the submitted text stands in. - if (this.ready) this.emit('exec', event.command || (this.adapter.id === 'zsh' ? '' : this.lastSubmitted), event.historyAllowed); + if (this.ready) { + const command = event.command || (this.adapter.id === 'zsh' ? '' : this.lastSubmitted); + this.inputWatch.onExec(command); + this.emit('exec', command, event.historyAllowed); + } } else if (!this.ready) { this.atPrompt = true; this.ready = true; @@ -222,7 +245,10 @@ export class ShellSession extends EventEmitter { if (pending && !this.exited) this.pty.write(pending); } else { this.atPrompt = true; - this.emit('prompt', this.withKnowledge(event.marker)); + // Final waiting time first, so it travels with the prompt that completes the command. + this.inputWatch.onPrompt(); + const {waitedMs, waits} = this.inputWatch.state.timing; + this.emit('prompt', {...this.withKnowledge(event.marker), ...(waits ? {inputWaitMs: Math.round(waitedMs), inputWaits: waits} : {})}); } } } diff --git a/src/status/inputStatus.ts b/src/status/inputStatus.ts new file mode 100644 index 00000000..7feff4c2 --- /dev/null +++ b/src/status/inputStatus.ts @@ -0,0 +1,88 @@ +import {displayWidth, truncateText} from '../util/text.js'; +import {formatDuration} from './commandTiming.js'; +import {foreground, UI_COLORS} from '../ui/palette.js'; +import {getCurrentGlyphMode} from '../ui/glyphs.js'; +import {commandWord} from '../session/liveStatus.js'; +import {waitedSoFar, type InputRequest, type InputState, type InputTiming} from '../session/inputState.js'; + +/** + * How NMSh shows a command that needs the person. One state (InputWatch) drives every surface; this module only + * words and paints it. The look is NMSh's attention vocabulary: ◆ (confirmed) or ◇ (likely) in the accent with + * weight, never animated, so it reads differently from the shimmering "Running" line with or without color. + */ +const RESET = '\u001b[0m'; +const BOLD = '\u001b[1m'; + +export function inputGlyph(request: Pick): string { + const safe = getCurrentGlyphMode() === 'safe'; + return request.confidence === 'confirmed' ? (safe ? '!' : '◆') : (safe ? '?' : '◇'); +} + +/** The program the person is answering: what the platform reports in the foreground, else the command's word. */ +export function inputProgram(request: InputRequest, command: string): string { + return request.program || commandWord(command) || 'the command'; +} + +/** "Waiting for input" (confirmed) or "Probably waiting for input" (likely): the words carry the certainty. */ +export function inputHeadline(request: Pick): string { + return request.confidence === 'confirmed' ? 'Waiting for input' : 'Probably waiting for input'; +} + +/** How typing reaches the program, in a few words. */ +export function inputHint(request: InputRequest, program: string): string { + if (request.confidence === 'likely') return `Enter sends a line to ${program}`; + switch (request.mode) { + case 'hidden': return `typing is hidden and goes straight to ${program} · Enter sends`; + case 'key': return `each key goes straight to ${program}`; + case 'line': return `Enter sends a line to ${program}`; + } +} + +/** + * The two rows of the live activity region while a command waits: the attention line (headline, program, how long + * it has waited, and the total when it differs) and the prompt it left open with how input reaches it. Fits + * `columns` by dropping the least important parts first. + */ +export function inputActivityRows(state: InputState & {request: InputRequest}, command: string, startedAt: number, now: number, columns: number): [string, string] { + const request = state.request; + const program = inputProgram(request, command); + const accent = foreground(UI_COLORS.accent); + const secondary = foreground(UI_COLORS.secondary); + const subtle = foreground(UI_COLORS.subtle); + // The wait comes from the session's clock and the total from this frontend's: never show a wait longer than the run. + const total = Math.max(0, now - startedAt); + const waitedMs = Math.min(Math.max(0, now - request.since), total); + const waiting = formatDuration(waitedMs); + const showTotal = total - waitedMs >= 1000; + const head = `${inputGlyph(request)} ${inputHeadline(request)}`; + const tails = [[program, waiting, ...(showTotal ? [`${formatDuration(total)} total`] : [])], [program, waiting], [waiting]]; + const tail = tails.find(parts => displayWidth(`${head} · ${parts.join(' · ')}`) <= columns) ?? []; + const first = `${accent}${BOLD}${truncateText(head, columns)}${RESET}${tail.length ? `${secondary} · ${tail.join(' · ')}${RESET}` : ''}`; + const prompt = request.prompt ?? ''; + const hint = inputHint(request, program); + const indent = ' '; + const room = Math.max(1, columns - displayWidth(indent)); + let second: string; + // The question outranks the hint: the hint only shows when both fit; otherwise the question takes the row. + if (!prompt) second = `${indent}${subtle}${truncateText(hint, room)}${RESET}`; + else if (displayWidth(`${prompt} · ${hint}`) <= room) second = `${indent}${BOLD}${prompt}${RESET}${subtle} · ${hint}${RESET}`; + else second = `${indent}${BOLD}${truncateText(prompt, room)}${RESET}`; + return [first, second]; +} + +/** A running line's extra detail once the command has waited before: " · waited 18s" (never more than `elapsedMs`). */ +export function waitedDetail(state: InputState | undefined, now: number, elapsedMs = Number.POSITIVE_INFINITY): string { + const waited = Math.min(waitedSoFar(state, now), elapsedMs); + return waited >= 1000 ? ` · waited ${formatDuration(waited)}` : ''; +} + +/** The completion fact: " · 18s waiting for input" (or "in 3 waits"); empty when the command never waited. */ +export function completionWaitFact(timing: InputTiming | undefined): string { + if (!timing?.waits || timing.waitedMs < 1000) return ''; + return ` · ${formatDuration(timing.waitedMs)} waiting for input${timing.waits > 1 ? ` (${timing.waits} prompts)` : ''}`; +} + +/** The composer's placeholder while keys go straight to the program (hidden or key input). Never shows what is typed. */ +export function directInputPlaceholder(request: InputRequest, program: string): string { + return request.mode === 'hidden' ? `Hidden input for ${program} · nothing you type is shown` : `Keys go straight to ${program}`; +} diff --git a/src/terminal/keyBytes.ts b/src/terminal/keyBytes.ts new file mode 100644 index 00000000..ca5ab363 --- /dev/null +++ b/src/terminal/keyBytes.ts @@ -0,0 +1,33 @@ +import type {Key} from './keys.js'; + +/** + * The bytes an ordinary terminal sends for a key, for a program reading the terminal directly (a hidden line or + * single keys) while NMSh's own terminal speaks an extended keyboard protocol. Only keys with one conventional + * encoding are sent; NMSh's own gestures (selection, history search, palette, focus) and pointer events are not, + * so nothing the program did not ask for reaches it. Ctrl+C and Ctrl+D are the caller's (signal and end of input). + */ +const PLAIN: Partial> = { + enter: '\r', + // Shift+Enter has no meaning to a program reading a line or keys: it submits, as Enter does. + newline: '\r', + backspace: '\u007f', + delete: '\u001b[3~', + complete: '\t', + focusPrevious: '\u001b[Z', + escape: '\u001b', + up: '\u001b[A', + down: '\u001b[B', + right: '\u001b[C', + left: '\u001b[D', + lineHome: '\u001b[H', + lineEnd: '\u001b[F', + // The line discipline's own editing keys: word and line erase. + deleteWord: '\u0017', + deleteLineBefore: '\u0015', + suspend: '\u001a', +}; + +export function directKeyBytes(key: Key): string | undefined { + if (key.kind === 'text' || key.kind === 'paste') return key.value; + return PLAIN[key.kind]; +} diff --git a/tests/fixtures/askyn.py b/tests/fixtures/askyn.py new file mode 100755 index 00000000..c3f0b79b --- /dev/null +++ b/tests/fixtures/askyn.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +"""A harmless stand-in for e2fsck's yes/no questions: the terminal in single-key, no-echo mode (as e2fsck's ask_yn sets +it), one key per answer, Enter takes the default. Repairs nothing; prints what it was told.""" +import os, sys, termios, tty + +questions = sys.argv[1:] or ['Padding at end of inode bitmap is not set. Fix? '] +fd = sys.stdin.fileno() +saved = termios.tcgetattr(fd) +answers = [] +try: + for question in questions: + mode = termios.tcgetattr(fd) + mode[3] &= ~(termios.ICANON | termios.ECHO) + mode[6][termios.VMIN], mode[6][termios.VTIME] = 1, 0 + termios.tcsetattr(fd, termios.TCSANOW, mode) + sys.stdout.write(question); sys.stdout.flush() + key = os.read(fd, 1).decode(errors='replace') + termios.tcsetattr(fd, termios.TCSANOW, saved) + answer = 'yes' if key in ('y', 'Y', '\r', '\n') else 'no' + answers.append(answer) + sys.stdout.write(answer + '\n\n'); sys.stdout.flush() +finally: + termios.tcsetattr(fd, termios.TCSANOW, saved) +print('ANSWERS=' + ','.join(answers)) diff --git a/tests/inputAwareness.test.ts b/tests/inputAwareness.test.ts new file mode 100644 index 00000000..afe82a4a --- /dev/null +++ b/tests/inputAwareness.test.ts @@ -0,0 +1,392 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import {OpenLine, promptShaped, promptText} from '../src/session/openLine.js'; +import {parseCpuTime, parseForeground, parseStty, parseSyscall, type TerminalSample} from '../src/session/terminalProbe.js'; +import {InputWatch} from '../src/session/InputWatch.js'; +import {directInput, waitedSoFar, type InputState} from '../src/session/inputState.js'; +import {decodeMessage, encodeMessage, inputStateFrom, inputStateMessage, parseClientFeatures, type SessionInfo} from '../src/session/SessionProtocol.js'; +import {describeNotice, noticeVisibleMs, SessionNoticeTracker} from '../src/session/SessionNotices.js'; +import {liveStatusParts} from '../src/session/liveStatus.js'; +import {LIVE_ROW_LABELS, liveRowState} from '../src/sessions/ResumeBrowser.js'; +import {directKeyBytes} from '../src/terminal/keyBytes.js'; +import {completionWaitFact, directInputPlaceholder, inputActivityRows, waitedDetail} from '../src/status/inputStatus.js'; +import {displayWidth, stripAnsi} from '../src/util/text.js'; + +function withEnv(patch: Record, run: () => T): T { + const saved = Object.fromEntries(Object.keys(patch).map(name => [name, process.env[name]])); + for (const [name, value] of Object.entries(patch)) { if (value === undefined) delete process.env[name]; else process.env[name] = value; } + try { return run(); } finally { + for (const [name, value] of Object.entries(saved)) { if (value === undefined) delete process.env[name]; else process.env[name] = value; } + } +} + +// ------------------------------------------------------------------ open line + +test('open line: a prompt left without a newline is open; a finished line, a rewrite or cursor drawing is not', () => { + const line = new OpenLine(); + line.push('Pass 1: Checking inodes\r\nPadding at end of inode bitmap is not set. Fix? '); + assert.equal(line.line, 'Padding at end of inode bitmap is not set. Fix? '); + assert.equal(line.open, true); + line.push('yes\r\n\r\n'); + assert.equal(line.open, false, 'the echoed answer ends the line'); + + line.push('[=====> ] 45%'); + line.push('\r[=======> ] 61%'); + assert.equal(line.open, false, 'a carriage-return progress bar rewrites its line'); + line.push('\r\n'); + line.push('\u001b[2K\u001b[1Gstatus: building'); + assert.equal(line.open, false, 'a line drawn with cursor addressing is not prompt evidence'); + line.push('\n'); + line.push('\u001b[32mContinue?\u001b[0m [y/N] '); + assert.equal(line.line, 'Continue? [y/N] ', 'colors are not text'); + assert.equal(line.open, true); +}); + +test('open line: escape sequences and CRLF split across reads; backspace edits; OSC titles are not text', () => { + const line = new OpenLine(); + line.push('Name: \u001b'); + line.push(']0;my title\u0007'); + assert.equal(line.line, 'Name: '); + line.push('ab\b'); + assert.equal(line.line, 'Name: a'); + line.push('\r'); + assert.equal(line.line, '', 'a lone carriage return may be a rewrite or the start of CRLF'); + line.push('\n'); + assert.equal(line.open, false); + assert.equal(line.unreliable, false, 'CRLF split across reads is a newline, not a rewrite'); +}); + +test('prompt shape: question and entry endings count; statuses and ellipses do not', () => { + for (const prompt of ['Fix? ', 'Password:', 'Continue? [Y/n] ', 'Overwrite (yes/no)', '>>> ', 'Choose 1-3 »']) assert.equal(promptShaped(prompt), true, prompt); + for (const status of ['Downloading...', 'Compiling crate…', 'building 45%', '']) assert.equal(promptShaped(status), false, status); + assert.equal(promptText(' Fix? '), 'Fix?'); + assert.equal(promptText('\u0007'), undefined); + assert.equal(promptText('x'.repeat(300))!.length, 120); +}); + +// ------------------------------------------------------------------ probe parsers + +test('stty: BSD and GNU layouts; flags are whole words', () => { + const bsd = 'speed 9600 baud; 24 rows; 80 columns;\nlflags: -icanon isig iexten -echo echoe echok echoke -echonl echoctl\n'; + assert.deepEqual(parseStty(bsd), {canonical: false, echo: false, signals: true}); + const gnu = 'speed 38400 baud; rows 24; columns 80; line = 0;\n-ignbrk -brkint\nisig icanon iexten -echo echoe echok -echonl -noflsh\n'; + assert.deepEqual(parseStty(gnu), {canonical: true, echo: false, signals: true}); + assert.equal(parseStty('lflags: echoe echok'), undefined, 'echoe is not echo'); +}); + +test('ps: only the terminal\'s foreground group; CPU time in both formats', () => { + const ps = ' 4101 4101 4120 Ss 0:00.21 /bin/zsh\n 4120 4120 4120 S+ 0:01.50 /opt/homebrew/sbin/e2fsck\n 4122 4121 4120 S 0:00.00 helper\n'; + assert.deepEqual(parseForeground(ps), [{pid: 4120, state: 'S+', cpuMs: 1500, command: 'e2fsck'}]); + assert.equal(parseCpuTime('1-02:03:04'), ((26 * 60 + 3) * 60 + 4) * 1000); + assert.equal(parseCpuTime('00:00:07'), 7000); + assert.equal(parseCpuTime('bad'), undefined); +}); + +test('Linux syscall report: a read of a descriptor, a poll/select wait, something else, or running', () => { + assert.deepEqual(parseSyscall('0 0x0 0x7ffd 0x1 0x0 0x0 0x0 0x7ffd 0x7f', 'x64'), {kind: 'read', fd: 0}); + assert.deepEqual(parseSyscall('63 0x3 0xffff 0x1 0 0 0 0xff 0xff', 'arm64'), {kind: 'read', fd: 3}); + assert.deepEqual(parseSyscall('7 0x7ffd 0x1 0xffffffff 0 0 0 0x1 0x2', 'x64'), {kind: 'wait'}); + assert.deepEqual(parseSyscall('230 0x0 0x0 0x7ffd 0 0 0 0x1 0x2', 'x64'), {kind: 'other'}, 'clock_nanosleep: sleeping, not reading'); + assert.equal(parseSyscall('running', 'x64'), undefined); + assert.equal(parseSyscall('-1 0x7ffd 0x1', 'x64'), undefined); +}); + +// ------------------------------------------------------------------ the watch + +/** A watch driven by hand: a clock, a timer queue and a probe answering with whatever the test sets. */ +function harness(command = 'e2fsck -f disk.img') { + let now = 1_000_000; + const timers: Array<{at: number; run: () => void}> = []; + let sample: TerminalSample | undefined = {foreground: []}; + const changes: InputState[] = []; + const watch = new InputWatch({probe: async () => sample, tty: () => '/dev/ttys999', now: () => now, + setTimer: (run, ms) => { const timer = {at: now + ms, run}; timers.push(timer); return () => { timers.splice(timers.indexOf(timer), 1); }; }}); + watch.on('change', state => changes.push(state)); + const advance = async (ms: number) => { + const until = now + ms; + for (;;) { + timers.sort((a, b) => a.at - b.at); + const next = timers[0]; + if (!next || next.at > until) break; + timers.shift(); + now = next.at; + next.run(); + // The probe resolves on a microtask. + for (let index = 0; index < 5; index++) await Promise.resolve(); + } + now = until; + }; + watch.onExec(command, now); + return {watch, changes, advance, set: (next: TerminalSample | undefined) => { sample = next; }, at: () => now, setNow: (value: number) => { now = value; }}; +} + +const fg = (state = 'S+', cpuMs = 100, command = 'e2fsck') => [{pid: 4120, state, cpuMs, command}]; +const keyModes = {canonical: false, echo: false, signals: true}; +const lineModes = {canonical: true, echo: true, signals: true}; +const hiddenModes = {canonical: true, echo: false, signals: true}; + +test('watch: single-key input with a prompt left open is confirmed at once, from when the prompt was written', async () => { + const h = harness(); + h.watch.onOutput('Pass 1\r\nPadding at end of inode bitmap is not set. Fix? ', h.at()); + const written = h.at(); + h.set({modes: keyModes, foreground: fg()}); + await h.advance(200); + const request = h.watch.state.request!; + assert.equal(request.confidence, 'confirmed'); + assert.equal(request.mode, 'key'); + assert.equal(request.prompt, 'Padding at end of inode bitmap is not set. Fix?'); + assert.equal(request.program, 'e2fsck'); + assert.equal(request.since, written); + assert.equal(directInput(request), true, 'keys go straight to the program'); + // One key answers; the wait is closed at that moment. + await h.advance(17_800); + h.watch.onInput('y', h.at()); + assert.equal(h.watch.state.request, undefined); + assert.equal(h.watch.state.timing.waitedMs, 18_000); + assert.equal(h.watch.state.timing.waits, 1); +}); + +test('watch: raw mode with no prompt left (a quiet docker -it server, a spinner between frames) is never a request', async () => { + const h = harness('docker run -it image'); + h.watch.onOutput('listening on :8080\r\n', h.at()); + h.set({modes: {canonical: false, echo: false, signals: false}, foreground: fg('S+', 100, 'docker')}); + await h.advance(30_000); + assert.equal(h.watch.state.request, undefined); +}); + +test('watch: a hidden line (echo off, line input on) is confirmed even with no prompt text', async () => { + const h = harness('read -s secret'); + h.set({modes: hiddenModes, foreground: fg('Ss+', 10, 'zsh')}); + await h.advance(500); + assert.equal(h.watch.state.request?.mode, 'hidden'); + assert.equal(h.watch.state.request?.confidence, 'confirmed'); + // Typing inside a hidden line does not end the wait; Enter does. + h.watch.onInput('hunter', h.at()); + assert.ok(h.watch.state.request, 'still waiting while the line is typed'); + h.watch.onInput('2\r', h.at()); + assert.equal(h.watch.state.request, undefined); +}); + +test('watch: ordinary line input is only "likely", and only after the group is seen idle twice with a question open', async () => { + const h = harness('rm -i notes.txt'); + h.watch.onOutput('remove notes.txt? ', h.at()); + h.set({modes: lineModes, foreground: fg('S+', 100, 'rm')}); + await h.advance(200); + assert.equal(h.watch.state.request, undefined, 'one sample cannot tell idle from busy'); + await h.advance(1000); + assert.equal(h.watch.state.request?.confidence, 'likely'); + assert.equal(h.watch.state.request?.mode, 'line'); + assert.equal(directInput(h.watch.state.request), false, 'a likely request keeps the composer\'s line editor'); + // Enter submits the line and ends the wait. + h.watch.onInput('y\r', h.at()); + assert.equal(h.watch.state.request, undefined); +}); + +test('watch: no false positives for builds, sleep, network waits, progress bars, statuses or log streams', async () => { + // A build that printed a colon-ended status and is computing. + const build = harness('make'); + build.watch.onOutput('Linking target:', build.at()); + let cpu = 0; + for (let index = 0; index < 10; index++) { + cpu += 900; + build.set({modes: lineModes, foreground: fg('S+', cpu, 'ld')}); + await build.advance(1000); + } + assert.equal(build.watch.state.request, undefined, 'busy CPU across samples'); + const running = harness('cargo build'); + running.watch.onOutput('Compiling serde:', running.at()); + running.set({modes: lineModes, foreground: fg('R+', 100, 'rustc')}); + await running.advance(10_000); + assert.equal(running.watch.state.request, undefined, 'a process on a CPU is not waiting'); + // sleep and a network wait: idle, but no question was left open. + for (const [command, output] of [['sleep 600', ''], ['curl https://example.com', ' % Total % Received\r\n'], ['tail -f log', 'GET /index 200\r\n']]) { + const quiet = harness(command!); + if (output) quiet.watch.onOutput(output, quiet.at()); + quiet.set({modes: lineModes, foreground: fg('S+', 5, 'x')}); + await quiet.advance(60_000); + assert.equal(quiet.watch.state.request, undefined, command); + } + // A stalled progress bar and a status line left open. + const stalled = harness('npm install'); + stalled.watch.onOutput('[====> ] 45%', stalled.at()); + stalled.watch.onOutput('\r[=====> ] 47%:', stalled.at()); + stalled.set({modes: lineModes, foreground: fg('S+', 5, 'node')}); + await stalled.advance(30_000); + assert.equal(stalled.watch.state.request, undefined, 'a rewritten line is not a question'); + const status = harness('fetch'); + status.watch.onOutput('Downloading...', status.at()); + status.set({modes: lineModes, foreground: fg('S+', 5, 'fetch')}); + await status.advance(30_000); + assert.equal(status.watch.state.request, undefined, 'an ellipsis is a status'); +}); + +test('watch: programs that own the terminal (full screen, bracketed paste, known full-screen commands) are never reported', async () => { + for (const [command, output] of [['htop', '\u001b[?1049hsomething?'], ['python3', '\u001b[?2004h>>> '], ['vim notes', 'Swap file exists. [O]pen? ']]) { + const h = harness(command!); + h.watch.onOutput(output!, h.at()); + h.set({modes: keyModes, foreground: fg('S+', 5, 'x')}); + await h.advance(5000); + assert.equal(h.watch.state.request, undefined, command); + assert.equal(h.watch.ownsTerminal, true, command); + } +}); + +test('watch: the kernel\'s report decides when it exists (Linux): reading confirms, blocked elsewhere vetoes', async () => { + const reading = harness('./menu.sh'); + reading.watch.onOutput('1) start\r\n2) stop\r\nChoice:\r\n', reading.at()); + reading.set({modes: lineModes, foreground: fg('S+', 5, 'bash'), readingTerminal: true}); + await reading.advance(500); + assert.equal(reading.watch.state.request?.confidence, 'confirmed', 'a prompt that ended with a newline is still a read'); + assert.equal(reading.watch.state.request?.mode, 'line'); + const elsewhere = harness('ssh-keygen'); + elsewhere.watch.onOutput('Enter passphrase: ', elsewhere.at()); + elsewhere.set({modes: hiddenModes, foreground: fg('S+', 5, 'ssh-keygen'), readingTerminal: false}); + await elsewhere.advance(5000); + assert.equal(elsewhere.watch.state.request, undefined); +}); + +test('watch: several questions in one command accumulate waiting time; completion closes an open wait', async () => { + const h = harness(); + h.set({modes: keyModes, foreground: fg()}); + h.watch.onOutput('Fix one? ', h.at()); + await h.advance(2000); + h.watch.onInput('y', h.at()); + h.watch.onOutput('yes\r\n\r\nFix two? ', h.at()); + await h.advance(3000); + assert.equal(h.watch.state.request?.prompt, 'Fix two?'); + assert.equal(waitedSoFar(h.watch.state, h.at()), 5000); + h.watch.onPrompt(h.at()); + assert.deepEqual(h.watch.state.timing, {waitedMs: 5000, waits: 2}); + assert.equal(h.watch.state.request, undefined); + // Ctrl+C ends a wait as an answer does. + const interrupted = harness('read x'); + interrupted.set({modes: hiddenModes, foreground: fg('S+', 5, 'zsh')}); + await interrupted.advance(600); + interrupted.watch.onInput('\u0003', interrupted.at()); + assert.equal(interrupted.watch.state.request, undefined); +}); + +test('watch: output after the prompt ends the wait; a probe begun before newer output is discarded', async () => { + const h = harness('slow'); + h.watch.onOutput('Continue? [y/N] ', h.at()); + let resolve!: (sample: TerminalSample) => void; + const pending = new Promise(done => { resolve = done; }); + const watch = new InputWatch({probe: () => pending, tty: () => '/dev/ttys1', now: () => h.at(), + setTimer: (run, ms) => { const timer = setTimeout(run, ms); return () => clearTimeout(timer); }}); + watch.onExec('slow', h.at()); + watch.onOutput('Continue? [y/N] ', h.at()); + await new Promise(done => setTimeout(done, 200)); + watch.onOutput('\r\nmoving on\r\n', h.at()); + resolve({modes: keyModes, foreground: fg()}); + await new Promise(done => setTimeout(done, 10)); + assert.equal(watch.state.request, undefined, 'the sample described the terminal before the newer output'); + watch.dispose(); + // An established wait ends when the program writes again. + h.set({modes: keyModes, foreground: fg()}); + await h.advance(200); + assert.ok(h.watch.state.request); + h.watch.onOutput('Timed out, assuming no\r\n', h.at() + 1000); + assert.equal(h.watch.state.request, undefined); + assert.equal(h.watch.state.timing.waits, 1); +}); + +test('watch: evidence that disappears without input or output (a timed-out read) ends the wait at that look', async () => { + const h = harness('read -t 5 -k1'); + h.watch.onOutput('Key? ', h.at()); + h.set({modes: keyModes, foreground: fg('S+', 5, 'zsh')}); + await h.advance(200); + assert.ok(h.watch.state.request); + h.set({modes: lineModes, foreground: [{pid: 9, state: 'S+', cpuMs: 5, command: 'sleep'}]}); + await h.advance(1000); + // Line input with a question still open and an idle group is the likely rule, not the old confirmed one. + assert.notEqual(h.watch.state.request?.confidence, 'confirmed'); +}); + +// ------------------------------------------------------------------ protocol, notices, surfaces + +test('protocol: input-state round-trips; unknown values are dropped, never guessed; hello features gate new values', () => { + const state: InputState = {request: {since: 5, confidence: 'confirmed', mode: 'hidden', prompt: 'Password:', program: 'sudo'}, timing: {waitedMs: 1200.4, waits: 1}}; + const decoded = decodeMessage(encodeMessage(inputStateMessage(state)).trimEnd()); + assert.ok(decoded.ok); + assert.deepEqual(inputStateFrom(decoded.message as never), {request: state.request, timing: {waitedMs: 1200, waits: 1}}); + assert.deepEqual(inputStateFrom({type: 'input-state', since: 5, confidence: 'certain', mode: 'line', waitedMs: 0, waits: 0}), {timing: {waitedMs: 0, waits: 0}}); + const hello = decodeMessage(encodeMessage({type: 'hello', version: 2, client: 'nmsh', features: 'input-state,future'}).trimEnd()); + assert.ok(hello.ok && hello.message.type === 'hello'); + assert.deepEqual([...parseClientFeatures((hello.message as {features?: string}).features)], ['input-state']); + assert.deepEqual([...parseClientFeatures(undefined)], [], 'an older frontend says nothing and gets nothing new'); + const prompt = decodeMessage(encodeMessage({type: 'prompt', exitCode: 0, cwd: '/', seq: 1, at: 2, inputWaitMs: 18000, inputWaits: 2}).trimEnd()); + assert.ok(prompt.ok && prompt.message.type === 'prompt' && (prompt.message as {inputWaitMs?: number}).inputWaitMs === 18000); +}); + +test('notices: one sticky input notice per wait, withdrawn when answered, worded by confidence', () => { + const tracker = new SessionNoticeTracker('s1'); + tracker.onExec('e2fsck -f disk.img', 100); + tracker.onInputNeeded(200, 'confirmed', 'e2fsck'); + const notice = tracker.notice!; + assert.equal(notice.kind, 'input'); + assert.equal(noticeVisibleMs(notice), undefined, 'sticky until answered or focused'); + assert.match(describeNotice(notice, 'Mango', 60_200).text, /^Mango · e2fsck is waiting for input · 1m · \/resume$/u); + tracker.onInputNeeded(200, 'confirmed'); + assert.equal(tracker.notice, notice, 'the same wait is never raised twice'); + tracker.onInputResolved(); + assert.equal(tracker.notice, undefined); + tracker.onInputNeeded(300, 'likely'); + assert.match(describeNotice(tracker.notice!, 'Mango', 400).text, /may be waiting for input/u); + tracker.clear(500); + tracker.onInputNeeded(450, 'confirmed'); + assert.equal(tracker.notice, undefined, 'a wait someone already saw by focusing the session is not news'); +}); + +test('/sessions and /resume read the service\'s state; nothing is re-guessed from quiet output', () => { + const base: SessionInfo = {id: 'a', pid: 1, state: 'detached', cwd: '/tmp', createdAt: 0, running: 'e2fsck -f disk.img', runningSince: 0, lastOutputAt: 0}; + const waiting = {...base, inputSince: 10_000, inputConfidence: 'confirmed', inputMode: 'key', inputPrompt: 'Fix?'}; + assert.equal(liveRowState(waiting, 70_000), 'input'); + assert.equal(LIVE_ROW_LABELS.input, 'Waiting for input'); + assert.equal(liveRowState({...waiting, inputConfidence: 'likely'}, 70_000), 'inputLikely'); + assert.match(liveStatusParts(waiting, 70_000).join(' · '), /waiting for input 1m · “Fix\?”/u); + assert.match(liveStatusParts(base, 70_000).join(' · '), /quiet/u, 'quiet output alone stays "quiet"'); + assert.equal(liveRowState(base, 70_000), 'running'); +}); + +test('direct keys: the bytes a plain terminal sends; NMSh gestures and pointer events send nothing', () => { + assert.equal(directKeyBytes({kind: 'text', value: 'y'}), 'y'); + assert.equal(directKeyBytes({kind: 'enter'}), '\r'); + assert.equal(directKeyBytes({kind: 'backspace'}), '\u007f'); + assert.equal(directKeyBytes({kind: 'deleteLineBefore'}), '\u0015'); + assert.equal(directKeyBytes({kind: 'up'}), '\u001b[A'); + assert.equal(directKeyBytes({kind: 'paste', value: 'secret'}), 'secret'); + for (const kind of ['selectAll', 'historySearch', 'palette', 'mouseClick', 'focusIn', 'toggleDetails'] as const) assert.equal(directKeyBytes({kind} as never), undefined, kind); +}); + +test('presentation: distinct from Running in words and glyphs; fits narrow widths; NO_COLOR and Safe glyphs keep it legible', () => { + const state = {request: {since: 82_000, confidence: 'confirmed' as const, mode: 'key' as const, prompt: 'Padding at end of inode bitmap is not set. Fix?', program: 'e2fsck'}, + timing: {waitedMs: 0, waits: 0}}; + withEnv({NO_COLOR: undefined, NMSH_ICONS: undefined, TERM: 'xterm-256color'}, () => { + const [first, second] = inputActivityRows(state, 'e2fsck -f disk.img', 0, 100_000, 100); + assert.equal(stripAnsi(first), '◆ Waiting for input · e2fsck · 18.0s · 1m 40s total'); + assert.equal(stripAnsi(second), ' Padding at end of inode bitmap is not set. Fix? · each key goes straight to e2fsck'); + for (const columns of [20, 32, 48]) { + for (const row of inputActivityRows(state, 'e2fsck -f disk.img', 0, 100_000, columns)) assert.ok(displayWidth(stripAnsi(row)) <= columns, `${columns}: ${stripAnsi(row)}`); + } + const narrow = inputActivityRows(state, 'e2fsck', 0, 100_000, 32).map(stripAnsi); + assert.match(narrow[0], /^◆ Waiting for input · 18\.0s$/u, 'the wait and its time survive; detail goes first'); + assert.match(narrow[1], /^ {2}Padding at end/u, 'the question outranks the hint'); + assert.equal(displayWidth(narrow[1]), 32, 'without the hint, the question takes the whole row'); + }); + withEnv({NO_COLOR: '1', NMSH_ICONS: 'safe'}, () => { + const [first, second] = inputActivityRows({...state, request: {...state.request, confidence: 'likely', mode: 'line'}}, 'rm -i x', 82_000, 100_000, 80); + assert.doesNotMatch(first + second, /\u001b\[38;/u, 'no color under NO_COLOR'); + assert.match(first, /\u001b\[1m/u, 'weight still marks it'); + assert.match(stripAnsi(first), /^\? Probably waiting for input · e2fsck · 18\.0s$/u); + assert.equal(stripAnsi(inputActivityRows(state, 'e2fsck', 0, 100_000, 80)[0]).charAt(0), '!'); + }); + assert.equal(directInputPlaceholder({...state.request, mode: 'hidden'}, 'sudo'), 'Hidden input for sudo · nothing you type is shown'); + assert.equal(completionWaitFact({waitedMs: 18_000, waits: 1}), ' · 18.0s waiting for input'); + assert.equal(completionWaitFact({waitedMs: 65_000, waits: 3}), ' · 1m 5s waiting for input (3 prompts)'); + assert.equal(completionWaitFact({waitedMs: 300, waits: 1}), '', 'an instant answer is not worth a fact'); + assert.equal(waitedDetail({timing: {waitedMs: 18_000, waits: 1}}, 0), ' · waited 18.0s'); + // The wait (session clock) is never shown longer than the run (this frontend's clock), e.g. after a reattach. + assert.match(stripAnsi(inputActivityRows(state, 'e2fsck', 91_000, 100_000, 100)[0]), /· 9\.0s$/u); + assert.equal(waitedDetail({timing: {waitedMs: 10_500, waits: 1}}, 0, 9000), ' · waited 9.0s'); + assert.equal(waitedDetail(undefined, 0), ''); +}); diff --git a/tests/inputAwarenessLive.test.ts b/tests/inputAwarenessLive.test.ts new file mode 100644 index 00000000..4b9e0b28 --- /dev/null +++ b/tests/inputAwarenessLive.test.ts @@ -0,0 +1,233 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {spawnSync} from 'node:child_process'; +import {existsSync, writeFileSync} from 'node:fs'; +import {join, resolve} from 'node:path'; +import {LiveSandbox, strip, until, type Frontend} from './helpers/liveFrontend.js'; + +/** + * Input awareness through the real composer, PTY and session service, with harmless fixtures only: the shells' + * own `read`, and tests/fixtures/askyn.py, which asks yes/no questions the way e2fsck does (single key, no echo, + * Enter takes the default) and repairs nothing. + */ +const ASKYN = resolve('tests/fixtures/askyn.py'); +const bash = ['/opt/homebrew/bin/bash', '/usr/local/bin/bash', '/usr/bin/bash', '/bin/bash'] + .find(path => existsSync(path) && /version (?:4\.[4-9]|[5-9]\.)/u.test(spawnSync(path, ['--version'], {encoding: 'utf8'}).stdout ?? '')); +const fish = ['/opt/homebrew/bin/fish', '/usr/local/bin/fish', '/usr/bin/fish'].find(existsSync); +const python = spawnSync('python3', ['-c', 'import termios'], {encoding: 'utf8'}).status === 0; +const supported = process.platform === 'darwin' || process.platform === 'linux'; + +type Shell = 'zsh' | 'bash' | 'fish'; + +async function start(shell: Shell, options: {size?: {cols: number; rows: number}; env?: Record; args?: string[]; sandbox?: LiveSandbox} = {}) { + const sandbox = options.sandbox ?? new LiveSandbox({provider: 'none', shellBackend: shell, themeBridge: {enabled: true, targets: {pager: {mode: 'follow'}}}}, + shell === 'bash' && bash ? {PATH: `${bash.replace(/\/bash$/u, '')}:${process.env.PATH}`} : {}); + const started = Date.now(); + const frontend = sandbox.launch(options.args ?? [], options.size ?? {cols: 100, rows: 30}, options.env ?? {}); + await frontend.waitFor(/Vespyr|notMyShell|zsh|bash|fish/u); + await until(() => existsSync(join(sandbox.config, 'nmsh', 'theme-bridge', `environment.${shell}`)) || frontend.output.length > 0 && Date.now() - started > 8000, 20_000, 'the managed shell'); + await frontend.run('echo READY', /READY/u); + return {sandbox, frontend}; +} + +/** Text the frontend drew after `from`, without escapes. */ +const drawn = (frontend: Frontend, from: number) => strip(frontend.output.slice(from)); +const pause = (ms: number) => new Promise(done => setTimeout(done, ms)); + +for (const shell of ['zsh', 'bash'] as const) { + const available = supported && (shell === 'zsh' || bash); + test(`live ${shell}: read prompts: a line (likely), a hidden line and a single key reach the program natively`, {skip: available ? false : `${shell} not installed`, timeout: 120_000}, async () => { + const {sandbox, frontend} = await start(shell); + try { + // An ordinary line: the evidence is a question left open while the shell idles, so NMSh says "probably". + let mark = frontend.mark; + frontend.pty.write(`${shell === 'zsh' ? "read 'x?Continue? [y/N] '" : "read -p 'Continue? [y/N] ' x"}; echo got=$x\r`); + await frontend.waitFor(/◇ Probably waiting for input · (?:zsh|bash)[\s\S]*Continue\? \[y\/N\]/u, mark); + // A considered answer (waits under a second are not worth a completion fact). + await pause(1200); + frontend.pty.write('y\r'); + await frontend.waitFor(/got=y[\s\S]*Completed · [\d.]+s · [\d.]+s waiting for input/u, mark); + + // A hidden line: keys go straight to the shell's read; nothing typed is drawn by NMSh. + mark = frontend.mark; + frontend.pty.write(`${shell === 'zsh' ? "read -s 'p?Password: '" : "read -s -p 'Password: ' p"}; echo; echo len=\${#p}\r`); + await frontend.waitFor(/◆ Waiting for input[\s\S]*Hidden input for (?:zsh|bash) · nothing you type is shown/u, mark); + const typed = frontend.mark; + frontend.pty.write('hunter2'); + await pause(300); + frontend.pty.write('\r'); + await frontend.waitFor(/len=7/u, mark); + assert.doesNotMatch(drawn(frontend, typed), /hunter2|•{3}/u, 'the secret is never drawn, not even as dots'); + + // A single key: answered without Enter, and no stray Enter reaches the shell afterwards. + mark = frontend.mark; + frontend.pty.write(`${shell === 'zsh' ? "read -k1 'k?Key: '" : "read -n1 -p 'Key: ' k"}; echo; echo key=$k\r`); + await frontend.waitFor(/◆ Waiting for input[\s\S]*Keys go straight to (?:zsh|bash)/u, mark); + frontend.pty.write('z'); + await frontend.waitFor(/key=z[\s\S]*Completed/u, mark); + await frontend.run('echo AFTER', /AFTER/u); + } finally { await sandbox.dispose(); } + }); +} + +/** + * Answer the terminal queries a program sends through NMSh (primary device attributes, cursor position), as a real + * terminal does. Fish's reader waits for these answers; the test PTY alone never sends them. + */ +function answerQueries(frontend: Frontend): () => void { + let seen = frontend.mark; + const timer = setInterval(() => { + const fresh = frontend.output.slice(seen); + seen = frontend.output.length; + for (const match of fresh.matchAll(/\u001b\[(0?c|6n)/gu)) frontend.pty.write(match[1] === '6n' ? '\u001b[1;1R' : '\u001b[?62;22c'); + }, 20); + return () => clearInterval(timer); +} + +test('live fish: read is fish\'s own line editor: handed the terminal, answered natively, never claimed as waiting', {skip: supported && fish ? false : 'fish not installed', timeout: 120_000}, async () => { + const {sandbox, frontend} = await start('fish'); + const stop = answerQueries(frontend); + try { + let mark = frontend.mark; + frontend.pty.write(`read -P 'Name: ' x; echo got=$x\r`); + // Fish's reader turns on bracketed paste: NMSh hands it the terminal, as for any interactive program. + await until(() => frontend.output.slice(mark).includes('Name: '), 15_000, 'fish draws its own prompt'); + frontend.pty.write('abc\r'); + await frontend.waitFor(/got=abc|Completed/u, mark); + mark = frontend.mark; + frontend.pty.write(`read -s -P 'Password: ' p; echo len=(string length -- $p)\r`); + await until(() => frontend.output.slice(mark).includes('Password: '), 15_000, 'fish draws its hidden prompt'); + frontend.pty.write('hunter2\r'); + await frontend.waitFor(/Completed/u, mark); + assert.doesNotMatch(frontend.output, /Waiting for input/u, 'a program that owns the terminal is not reported as waiting'); + // Known, pre-existing (reproduced on release/v0.18.0 without input awareness): under load, keys typed within about + // a second of fish returning from a handed-over `read` to its own prompt can be lost. Not part of this feature. + await pause(1500); + await frontend.run('echo AFTER-READ', /AFTER-READ/u); + // External programs under fish get the same awareness as under zsh. + if (python) { + mark = frontend.mark; + frontend.pty.write(`python3 ${ASKYN} 'Fix? '\r`); + await frontend.waitFor(/◆ Waiting for input[\s\S]*Fix\?/u, mark); + frontend.pty.write('n'); + await frontend.waitFor(/ANSWERS=no[\s\S]*Completed/u, mark); + } + } finally { stop(); await sandbox.dispose(); } +}); + +test('live: an e2fsck-style yes/no session: each key answers one question, Enter only when pressed; timing stays truthful', {skip: supported && python ? false : 'python3 with termios not available', timeout: 120_000}, async () => { + const {sandbox, frontend} = await start('zsh'); + try { + const mark = frontend.mark; + const started = Date.now(); + frontend.pty.write(`python3 ${ASKYN} 'Padding at end of inode bitmap is not set. Fix? ' 'Inode 12 ref count is 2, should be 1. Fix? ' 'Free blocks count wrong. Fix? '\r`); + await frontend.waitFor(/◆ Waiting for input · Python[\s\S]*Padding at end of inode bitmap is not set\. Fix\? · each key goes straight to Python/u, mark); + assert.match(drawn(frontend, mark), /Keys go straight to Python/u, 'the composer says where typing goes'); + await pause(1200); + frontend.pty.write('n'); + await frontend.waitFor(/Inode 12 ref count is 2, should be 1\. Fix\? · each key/u, mark); + const third = frontend.mark; + frontend.pty.write('\r'); + // A fast typist answers the moment the question appears, before NMSh knows how it reads: the key still reaches it. + await until(() => frontend.output.slice(third).includes('Free blocks count wrong'), 15_000, 'the third question'); + frontend.pty.write('y'); + await frontend.waitFor(/ANSWERS=no,yes,yes[\s\S]*Completed · [\d.]+s · [\d.]+s waiting for input \(3 prompts\)/u, mark); + const completed = /Completed · ([\d.]+)s · ([\d.]+)s waiting for input \(3 prompts\)/u.exec(drawn(frontend, mark)); + assert.ok(completed, drawn(frontend, mark).slice(-400)); + const [total, waited] = [Number(completed[1]), Number(completed[2])]; + assert.ok(waited <= total && total <= (Date.now() - started) / 1000 + 0.5, `waited ${waited}s within ${total}s`); + assert.ok(waited >= 1, 'the deliberate pause is counted as waiting'); + // Nothing leaked to the shell: the composer is empty and the next command runs normally. + await frontend.run('echo NEXT', /NEXT/u); + + // Ctrl+C at a waiting prompt interrupts the program, exactly as in a terminal. + const interrupt = frontend.mark; + frontend.pty.write(`python3 ${ASKYN} 'Fix? '\r`); + await frontend.waitFor(/◆ Waiting for input/u, interrupt); + frontend.pty.write('\u0003'); + await frontend.waitFor(/Interrupted|exit 130|KeyboardInterrupt/u, interrupt); + await frontend.run('echo STILL-HERE', /STILL-HERE/u); + } finally { await sandbox.dispose(); } +}); + +test('live: no false positives: sleep, a streaming build, a status left open, a full-screen pager', {skip: supported ? false : 'unsupported platform', timeout: 120_000}, async () => { + const {sandbox, frontend} = await start('zsh'); + try { + const mark = frontend.mark; + await frontend.run('sleep 3', /Completed/u); + await frontend.run('for i in 1 2 3 4 5 6; do echo compiling unit $i; sleep 0.5; done', /compiling unit 6[\s\S]*Completed/u); + await frontend.run(`printf 'Downloading...'; sleep 3; echo ' done'`, /done[\s\S]*Completed/u); + writeFileSync(join(sandbox.home, 'notes.txt'), 'line one\nline two\n'); + const pager = frontend.mark; + frontend.pty.write('less notes.txt\r'); + await until(() => frontend.output.slice(pager).includes('line two'), 15_000, 'less draws'); + await pause(2000); + frontend.pty.write('q'); + await frontend.waitFor(/Completed/u, pager); + assert.doesNotMatch(drawn(frontend, mark), /Waiting for input|waiting for input/u); + } finally { await sandbox.dispose(); } +}); + +test('live: a waiting session is visible from another window, survives detach, and is answered after reattach', {skip: supported && python ? false : 'python3 with termios not available', timeout: 150_000}, async () => { + const {sandbox, frontend: first} = await start('zsh'); + try { + const mark = first.mark; + first.pty.write(`python3 ${ASKYN} 'First question. Fix? ' 'Second question. Fix? '\r`); + await first.waitFor(/◆ Waiting for input/u, mark); + const [session] = await sandbox.sessions(); + assert.equal(session!.inputConfidence, 'confirmed'); + assert.equal(session!.inputMode, 'key'); + assert.equal(session!.inputPrompt, 'First question. Fix?'); + + // Another window: one notice once the wait has lasted, and /sessions says it in words. + const other = sandbox.launch(['--new']); + await other.waitFor(/❯/u); + await other.waitFor(/is waiting for input · .* · \/resume/u, 0, 30_000); + const list = other.mark; + other.pty.write('/sessions\r'); + await other.waitFor(/Waiting for input[\s\S]*First question/u, list); + other.pty.write('\u001b'); + + // The window goes away; the question keeps waiting, detached. + first.pty.kill('SIGKILL'); + await until(async () => (await sandbox.sessions()).find(item => item.id === session!.id)?.state === 'detached', 15_000, 'detached'); + assert.equal((await sandbox.sessions()).find(item => item.id === session!.id)?.inputSince, session!.inputSince, 'the same wait, not a new one'); + const back = sandbox.launch(['--attach', session!.id]); + await back.waitFor(/◆ Waiting for input[\s\S]*First question\. Fix\?/u, 0, 30_000); + const answered = back.mark; + back.pty.write('y'); + await back.waitFor(/Second question\. Fix\? · each key/u, answered); + back.pty.write('n'); + await back.waitFor(/ANSWERS=yes,no[\s\S]*Completed[\s\S]*waiting for input \(2 prompts\)/u); + } finally { await sandbox.dispose(); } +}); + +test('live: narrow, NO_COLOR and Safe glyphs keep the request legible', {skip: supported && python ? false : 'python3 with termios not available', timeout: 120_000}, async () => { + const {sandbox, frontend} = await start('zsh', {size: {cols: 44, rows: 20}, env: {NO_COLOR: '1', NMSH_ICONS: 'safe'}}); + try { + const mark = frontend.mark; + frontend.pty.write(`python3 ${ASKYN} 'Fix? '\r`); + await frontend.waitFor(/! Waiting for input[\s\S]*Fix\?/u, mark); + const painted = frontend.output.slice(mark); + assert.doesNotMatch(painted.slice(painted.lastIndexOf('Waiting for input') - 40), /\u001b\[38;[25];/u, 'no color under NO_COLOR'); + assert.doesNotMatch(drawn(frontend, mark), /◆|◇/u, 'Safe glyphs only'); + frontend.pty.write('y'); + await frontend.waitFor(/ANSWERS=yes/u, mark); + } finally { await sandbox.dispose(); } +}); + +test('live: a reply typed for a command that finishes first is discarded, never run as a shell command', {skip: supported ? false : 'unsupported platform', timeout: 120_000}, async () => { + const {sandbox, frontend} = await start('zsh'); + try { + const marker = join(sandbox.home, 'must-not-exist'); + const mark = frontend.mark; + frontend.pty.write(`read -t 4 'x?Quick? ' || echo TIMED-OUT\r`); + await frontend.waitFor(/Probably waiting for input/u, mark); + frontend.pty.write(`touch ${marker}`); + await frontend.waitFor(/reply was discarded, not run/u, mark, 20_000); + frontend.pty.write('\r'); + await pause(1500); + assert.equal(existsSync(marker), false); + await frontend.run('echo CLEAN', /CLEAN/u); + } finally { await sandbox.dispose(); } +}); From ce1f57d43c4a5eca085831ca7d3219b1f5bbcd08 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:11:30 +0530 Subject: [PATCH 017/249] Live fish test: wait for each command's own completion; no pause The pause stood in for a Fish keystroke loss that was the test typing ahead: a bare /Completed/ matched the previous command's row, so the next command was typed while read -s still ran. The loss itself is fixed on the release branch. --- tests/inputAwarenessLive.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/inputAwarenessLive.test.ts b/tests/inputAwarenessLive.test.ts index 4b9e0b28..46f4825c 100644 --- a/tests/inputAwarenessLive.test.ts +++ b/tests/inputAwarenessLive.test.ts @@ -93,16 +93,15 @@ test('live fish: read is fish\'s own line editor: handed the terminal, answered // Fish's reader turns on bracketed paste: NMSh hands it the terminal, as for any interactive program. await until(() => frontend.output.slice(mark).includes('Name: '), 15_000, 'fish draws its own prompt'); frontend.pty.write('abc\r'); - await frontend.waitFor(/got=abc|Completed/u, mark); + await frontend.waitFor(/got=abc[\s\S]*Completed/u, mark); mark = frontend.mark; frontend.pty.write(`read -s -P 'Password: ' p; echo len=(string length -- $p)\r`); await until(() => frontend.output.slice(mark).includes('Password: '), 15_000, 'fish draws its hidden prompt'); frontend.pty.write('hunter2\r'); - await frontend.waitFor(/Completed/u, mark); + // This command's own completion: a bare "Completed" could match the previous command's row. + await frontend.waitFor(/len=7[\s\S]*Completed/u, mark); assert.doesNotMatch(frontend.output, /Waiting for input/u, 'a program that owns the terminal is not reported as waiting'); - // Known, pre-existing (reproduced on release/v0.18.0 without input awareness): under load, keys typed within about - // a second of fish returning from a handed-over `read` to its own prompt can be lost. Not part of this feature. - await pause(1500); + // The next command follows at once (tests/typingAfterRead.test.ts covers this across shells). await frontend.run('echo AFTER-READ', /AFTER-READ/u); // External programs under fish get the same awareness as under zsh. if (python) { From 6fd296434ad26568db33cde291b59aad3522fc44 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Sat, 10 Oct 2026 11:03:37 +0530 Subject: [PATCH 018/249] Input awareness: a prompt whose evidence lapses is one prompt, its time counted once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI found it on macOS: after a reattach, the completion row read '5.7s · 7.6s waiting for input (3 prompts)'. Reattaching resizes the PTY; the waiting program wakes for SIGWINCH and one look sees it running, so the wait closed and was found again, restarting at the old prompt time and counted as a new prompt. A new wait now never starts before the previous one ended, and a prompt is counted once per stretch of output or input. The live tests expect what each platform can know: on Linux (first run in CI) the kernel confirms an ordinary line read and names the program python3; macOS infers it and says Python. --- src/session/InputWatch.ts | 19 +++++++++++++++++-- tests/inputAwareness.test.ts | 28 ++++++++++++++++++++++++++++ tests/inputAwarenessLive.test.ts | 14 ++++++++++---- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/session/InputWatch.ts b/src/session/InputWatch.ts index 59d9ed09..a454f25f 100644 --- a/src/session/InputWatch.ts +++ b/src/session/InputWatch.ts @@ -67,6 +67,15 @@ export class InputWatch extends EventEmitter { private request?: InputRequest; private waitedMs = 0; private waits = 0; + /** + * Changes with every output and every input: one prompt is one epoch. Evidence that lapses and returns within an + * epoch (a resize waking the program for a moment) continues the same prompt; it is never counted twice. + */ + private epoch = 0; + private requestEpoch = -1; + private countedEpoch = -1; + /** When the last wait ended: a new wait never starts earlier, so no time is counted twice. */ + private lastWaitEnd = 0; private lastActivity = 0; private lastOutput = 0; /** Bumped by every stream event; a probe that started before the latest event is stale. */ @@ -100,6 +109,8 @@ export class InputWatch extends EventEmitter { this.running = {command, since: at}; this.waitedMs = 0; this.waits = 0; + this.epoch += 1; + this.lastWaitEnd = 0; this.line.reset(); this.screen.reset(); this.previous.clear(); @@ -114,6 +125,7 @@ export class InputWatch extends EventEmitter { this.screen.push(data); this.line.push(data); this.lastOutput = at; + this.epoch += 1; // Output after the prompt: the program moved on (or asks again, which the next look sees). if (this.request) this.closeWait(at, true); this.touch(at); @@ -124,6 +136,7 @@ export class InputWatch extends EventEmitter { if (!this.running) return; const request = this.request; if (request && (request.mode === 'key' || /[\r\n\u0003\u0004\u001a]/u.test(data))) this.closeWait(at, true); + this.epoch += 1; this.touch(at); } @@ -150,7 +163,8 @@ export class InputWatch extends EventEmitter { if (!request) return; this.request = undefined; this.waitedMs += Math.max(0, at - request.since); - this.waits += 1; + this.lastWaitEnd = Math.max(this.lastWaitEnd, at); + if (this.requestEpoch !== this.countedEpoch) { this.waits += 1; this.countedEpoch = this.requestEpoch; } if (emit) this.emit('change', this.state); } @@ -205,10 +219,11 @@ export class InputWatch extends EventEmitter { return; } const program = sample.foreground.find(item => item.command)?.command; - const request: InputRequest = {since: current?.since ?? Math.max(this.running.since, this.lastOutput), confidence: next.confidence, mode: next.mode, + const request: InputRequest = {since: current?.since ?? Math.max(this.running.since, this.lastOutput, this.lastWaitEnd), confidence: next.confidence, mode: next.mode, ...(promptText(this.line.line) ? {prompt: promptText(this.line.line)} : {}), ...(program ? {program: program.slice(0, 64)} : {})}; if (current && current.confidence === request.confidence && current.mode === request.mode && current.prompt === request.prompt && current.program === request.program) return; + if (!current) this.requestEpoch = this.epoch; this.request = request; this.emit('change', this.state); } diff --git a/tests/inputAwareness.test.ts b/tests/inputAwareness.test.ts index afe82a4a..c6065dae 100644 --- a/tests/inputAwareness.test.ts +++ b/tests/inputAwareness.test.ts @@ -302,6 +302,34 @@ test('watch: evidence that disappears without input or output (a timed-out read) assert.notEqual(h.watch.state.request?.confidence, 'confirmed'); }); +test('watch: a prompt whose evidence lapses for a moment (a resize waking the program) is one prompt, its time counted once', async () => { + const h = harness('python3 askyn.py'); + const asked = h.at(); + h.watch.onOutput('First question. Fix? ', asked); + h.set({modes: keyModes, foreground: fg('S+', 10, 'python3')}); + await h.advance(5000); + assert.equal(h.watch.state.request?.since, asked); + // Reattaching resizes the PTY: the program wakes for SIGWINCH and one look sees it running. + h.set({modes: keyModes, foreground: fg('R+', 40, 'python3')}); + await h.advance(3000); + assert.equal(h.watch.state.request, undefined); + const lapsed = h.watch.state.timing.waitedMs; + h.set({modes: keyModes, foreground: fg('S+', 40, 'python3')}); + await h.advance(3000); + const reopened = h.watch.state.request; + assert.ok(reopened); + assert.ok(reopened.since >= asked + lapsed, 'the reopened wait starts where the last one ended, never at the old prompt'); + h.watch.onInput('y', h.at()); + h.watch.onOutput('yes\r\nSecond question. Fix? ', h.at()); + h.set({modes: keyModes, foreground: fg('S+', 41, 'python3')}); + await h.advance(300); + h.watch.onInput('n', h.at()); + const elapsed = h.at() - asked; + const {waitedMs, waits} = h.watch.state.timing; + assert.equal(waits, 2, 'two questions, two prompts'); + assert.ok(waitedMs <= elapsed, `waiting (${waitedMs} ms) never exceeds the time since the first question (${elapsed} ms)`); +}); + // ------------------------------------------------------------------ protocol, notices, surfaces test('protocol: input-state round-trips; unknown values are dropped, never guessed; hello features gate new values', () => { diff --git a/tests/inputAwarenessLive.test.ts b/tests/inputAwarenessLive.test.ts index 46f4825c..98e6ca22 100644 --- a/tests/inputAwarenessLive.test.ts +++ b/tests/inputAwarenessLive.test.ts @@ -16,6 +16,12 @@ const bash = ['/opt/homebrew/bin/bash', '/usr/local/bin/bash', '/usr/bin/bash', const fish = ['/opt/homebrew/bin/fish', '/usr/local/bin/fish', '/usr/bin/fish'].find(existsSync); const python = spawnSync('python3', ['-c', 'import termios'], {encoding: 'utf8'}).status === 0; const supported = process.platform === 'darwin' || process.platform === 'linux'; +/** + * What each platform can know (docs/design/input-awareness.md): Linux's kernel confirms an ordinary line read (◆), + * macOS can only infer it (◇ Probably). The foreground program is named as the platform reports it. + */ +const LINE_WAIT = process.platform === 'linux' ? '◆ Waiting for input' : '◇ Probably waiting for input'; +const PYTHON = process.platform === 'linux' ? 'python3' : 'Python'; type Shell = 'zsh' | 'bash' | 'fish'; @@ -42,7 +48,7 @@ for (const shell of ['zsh', 'bash'] as const) { // An ordinary line: the evidence is a question left open while the shell idles, so NMSh says "probably". let mark = frontend.mark; frontend.pty.write(`${shell === 'zsh' ? "read 'x?Continue? [y/N] '" : "read -p 'Continue? [y/N] ' x"}; echo got=$x\r`); - await frontend.waitFor(/◇ Probably waiting for input · (?:zsh|bash)[\s\S]*Continue\? \[y\/N\]/u, mark); + await frontend.waitFor(new RegExp(`${LINE_WAIT} · (?:zsh|bash)[\\s\\S]*Continue\\? \\[y/N\\]`, 'u'), mark); // A considered answer (waits under a second are not worth a completion fact). await pause(1200); frontend.pty.write('y\r'); @@ -120,8 +126,8 @@ test('live: an e2fsck-style yes/no session: each key answers one question, Enter const mark = frontend.mark; const started = Date.now(); frontend.pty.write(`python3 ${ASKYN} 'Padding at end of inode bitmap is not set. Fix? ' 'Inode 12 ref count is 2, should be 1. Fix? ' 'Free blocks count wrong. Fix? '\r`); - await frontend.waitFor(/◆ Waiting for input · Python[\s\S]*Padding at end of inode bitmap is not set\. Fix\? · each key goes straight to Python/u, mark); - assert.match(drawn(frontend, mark), /Keys go straight to Python/u, 'the composer says where typing goes'); + await frontend.waitFor(new RegExp(`◆ Waiting for input · ${PYTHON}[\\s\\S]*Padding at end of inode bitmap is not set\\. Fix\\? · each key goes straight to ${PYTHON}`, 'u'), mark); + assert.match(drawn(frontend, mark), new RegExp(`Keys go straight to ${PYTHON}`, 'u'), 'the composer says where typing goes'); await pause(1200); frontend.pty.write('n'); await frontend.waitFor(/Inode 12 ref count is 2, should be 1\. Fix\? · each key/u, mark); @@ -221,7 +227,7 @@ test('live: a reply typed for a command that finishes first is discarded, never const marker = join(sandbox.home, 'must-not-exist'); const mark = frontend.mark; frontend.pty.write(`read -t 4 'x?Quick? ' || echo TIMED-OUT\r`); - await frontend.waitFor(/Probably waiting for input/u, mark); + await frontend.waitFor(new RegExp(LINE_WAIT, 'u'), mark); frontend.pty.write(`touch ${marker}`); await frontend.waitFor(/reply was discarded, not run/u, mark, 20_000); frontend.pty.write('\r'); From 66b60f3ce76560b1b747f77c87ca2edb9d1cf416 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Sat, 10 Oct 2026 09:00:53 +0530 Subject: [PATCH 019/249] NMSh's own lines wrap at words in narrow windows /copy confirmations, notices and completion rows split mid-word at narrow widths ('5 l' / 'ines'). Lines NMSh writes itself now break after a space, with continuation rows hanging under the text after a leading glyph; a word wider than the row still breaks by character. Each continuation row's indent covers only added padding, so a selection copies the line exactly. Shell output keeps the terminal's column wrapping. Live tests that read joined screen rows match these messages across a word break. --- CHANGELOG.md | 1 + src/output/TranscriptPresenter.ts | 6 ++-- src/output/viewport.ts | 60 +++++++++++++++++++++++++++++++ tests/liveHardening.test.ts | 2 +- tests/noticeWordWrap.test.ts | 40 +++++++++++++++++++++ tests/startupDiscovery.test.ts | 4 +-- 6 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 tests/noticeWordWrap.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ae92aa6..322d01a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ Context Engine platform, Status Strip 2.0, managed Claude Code, worktrees and a - An absolute executable path typed in the composer (`/Volumes/tools/adb devices`, `/bin/zsh`, `/usr/bin/env …`) runs in the shell exactly as typed instead of being refused as an unknown NMSh command, and keeps shell completion and suggestions. One text-only rule decides: a first word with a `/` after the leading one is a path (no NMSh command name contains one), so `/zsh` still switches shells while `/bin/zsh` runs; a directory used as a command gets the shell's own behavior. Verified end to end on zsh, Bash and Fish. - Fish: keys typed quickly around a `read` are no longer lost to NMSh's round trip. Fish's `read` is fish's own line editor, which queries the terminal and drops keys that arrive while it waits for the answer. NMSh now answers those queries itself, as it already did at fish's prompt, instead of routing them through the host terminal. For any program, keys never overtake the answer to a device-attributes query. Type-ahead while a fish `read` is still running can still be dropped by fish itself, as in any terminal (`docs/development/fish-typeahead.md`). - After a screen clear (`clear`, tmux, or any program writing `ESC[2J`/`ESC[3J`), fold rows such as `15 lines shown · Ctrl+O` no longer appear inside the next command's output, and Ctrl+O no longer folds another command's lines together with `/copy` and status rows. The clear removed the transcript's lines but left the earlier commands' records pointing at line numbers that the new commands then reused. Those records now go with their lines. A command that clears the screen owns, and `/copy` copies, what it writes afterwards. Transcripts saved before this fix are repaired when opened. A record is kept only if its range fits, its header lines are command lines and the lines it covers read as the output it recorded. The first record that fails marks an unrecorded clear, so it and everything older lose their fold control and `/copy N`; their lines stay visible. Full-screen programs, partial erases (`ESC[J`, `ESC[1J`, `ESC[K`) and progress redraws never clear the transcript. +- NMSh's own lines (`/copy` confirmations, notices, completion rows) wrap at word boundaries in narrow windows, with continuation rows indented under the text instead of splitting words such as `5 l`/`ines`. Copying a selection still gives the line exactly as stored. Shell output keeps the terminal's column wrapping. - `/copy` and `/copy N` copy only the command's own output (stdout and stderr, line breaks and spacing kept), never NMSh's completion row such as `✔ Completed · 21.6s · 15:42`; output that merely looks like one is kept. A command that printed nothing leaves the clipboard as it was and says so. The block actions already used the same source. - Ask's config edits no longer refuse files in a home folder or project reached through a symlink (macOS `/tmp` and `/var` are symlinks); a symlink leading out of them is still refused. diff --git a/src/output/TranscriptPresenter.ts b/src/output/TranscriptPresenter.ts index 7d1a79b3..bfdf5c79 100644 --- a/src/output/TranscriptPresenter.ts +++ b/src/output/TranscriptPresenter.ts @@ -3,7 +3,7 @@ import {askFoldLabel} from '../ask/transcriptSummary.js'; import {paintDivider, DEFAULT_TREATMENT_SETTINGS, type TreatmentSettings} from '../chroma/treatment.js'; import {HyperlinkPresenter} from './Hyperlinks.js'; import {type StyledLine} from './AnsiOutputParser.js'; -import {wrapStyledLine, type WrappedRow} from './viewport.js'; +import {wrapStyledLine, wrapStyledWords, type WrappedRow} from './viewport.js'; import {background, foreground, UI_COLORS, lazyForeground} from '../ui/palette.js'; import {mixRgb} from '../chroma/chroma.js'; import {GLYPHS} from '../ui/glyphs.js'; @@ -97,7 +97,9 @@ export class TranscriptPresenter { const owner = view.ownerOf(index); const context = owner === undefined ? undefined : view.historicalContexts.get(owner); const line = view.lines[index] ?? []; - return wrapStyledLine(this.hyperlinks ? this.links.line(line, context?.cwd) : line, width, this.hyperlinks); + // NMSh's own lines wrap at words; shell output wraps by column, as a terminal does. + const wrap = view.lineTypes.get(index) === 'metadata' ? wrapStyledWords : wrapStyledLine; + return wrap(this.hyperlinks ? this.links.line(line, context?.cwd) : line, width, this.hyperlinks); } private treatment = DEFAULT_TREATMENT_SETTINGS; diff --git a/src/output/viewport.ts b/src/output/viewport.ts index 0bbec302..e15c9b25 100644 --- a/src/output/viewport.ts +++ b/src/output/viewport.ts @@ -86,6 +86,66 @@ export function wrapStyledLine(line: StyledLine, width: number, hyperlinks = fal return rows; } +const isBlank = (cell: StyledCell | null | undefined) => cell === undefined || (cell !== null && cell.text === ' '); + +/** + * Word wrapping for NMSh's own lines (notices, confirmations, lifecycle rows): breaks fall after a space, and + * continuation rows hang under the text after a leading glyph (` ⎿ Copied … · 5` / ` lines`). A word wider than + * the row still breaks by character. Shell output never comes here: it wraps by column, as a terminal does. + * + * Content is exact: every cell lands on exactly one row, and each continuation row's `indent` covers only the + * padding NMSh added, so selection, which joins one line's rows and drops that indent, reads the line as stored. + * A space that falls at a break and does not fit is drawn as the last column of the next row's padding. + */ +export function wrapStyledWords(line: StyledLine, width: number, hyperlinks = false): WrappedRow[] { + if (width <= 0) return []; + const cellWidth = (cell: StyledCell | null | undefined) => cell === null ? 0 : (cell?.width ?? 1); + const total = line.reduce((sum, cell) => sum + cellWidth(cell), 0); + if (total <= width) return wrapStyledLine(line, width, hyperlinks); + // Hang under the text: leading blanks, then one glyph and its space when the line starts with one. + let lead = 0; + while (lead < line.length && isBlank(line[lead])) lead += 1; + const glyph = line[lead]; + if (glyph && glyph.text !== ' ' && glyph.width === 1 && isBlank(line[lead + 1])) lead += 2; + const hang = lead <= width / 2 ? lead : 0; + + const segments: Array<{cells: StyledLine; leadingSpace: boolean}> = []; + let start = 0; + let leadingSpace = false; + while (start < line.length) { + const available = segments.length === 0 ? width : width - hang; + let column = 0; + let breakAfter = -1; + let index = start; + for (; index < line.length; index += 1) { + const cell = line[index]; + if (column + cellWidth(cell) > available) break; + column += cellWidth(cell); + if (isBlank(cell)) breakAfter = index; + } + if (index >= line.length) { segments.push({cells: line.slice(start), leadingSpace}); break; } + if (isBlank(line[index]) && hang > 0) { + // The break falls on a space that does not fit: it becomes the last column of the next row's padding. + segments.push({cells: line.slice(start, index), leadingSpace}); + start = index + 1; + leadingSpace = true; + continue; + } + const end = breakAfter >= start ? breakAfter + 1 : Math.max(index, start + 1); + segments.push({cells: line.slice(start, end), leadingSpace}); + start = end; + leadingSpace = false; + } + return segments.flatMap((segment, position) => { + if (position === 0) return wrapStyledLine(segment.cells, width, hyperlinks); + const padding = ' '.repeat(hang); + // A carried space is content: it is the last padding column, outside the indent selection drops. + const indent = segment.leadingSpace ? hang - 1 : hang; + return wrapStyledLine(segment.cells, width - hang, hyperlinks) + .map(row => ({...row, ansi: `${padding}${row.ansi}`, plain: `${padding}${row.plain}`, ...(indent > 0 ? {indent} : {})})); + }); +} + export function wrapLines(lines: StyledLine[], width: number): WrappedRow[] { return lines.flatMap(line => wrapStyledLine(line, width)); } diff --git a/tests/liveHardening.test.ts b/tests/liveHardening.test.ts index dbbdd560..419dfd07 100644 --- a/tests/liveHardening.test.ts +++ b/tests/liveHardening.test.ts @@ -152,7 +152,7 @@ test('the live-session limit falls back factually and never ends detached sessio await until(async () => (await sandbox.sessions()).filter(s => s.state === 'detached').length === 2, 15000, '2 detached'); pids.push(...(await sandbox.sessions()).map(s => s.pid)); const third = sandbox.launch(['--new']); - await third.waitFor(/live sessions are already running \(the limit\)/); + await third.waitFor(/live\s+sessions\s+are\s+already\s+running\s+\(the\s+limit\)/); await third.waitFor(/cannot be detached or reattached/); await third.run('echo MODE=$NMSH_SESSION_MODE', /MODE=in-process/); assert.ok(pids.every(processAlive)); diff --git a/tests/noticeWordWrap.test.ts b/tests/noticeWordWrap.test.ts new file mode 100644 index 00000000..b888208e --- /dev/null +++ b/tests/noticeWordWrap.test.ts @@ -0,0 +1,40 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {OutputBuffer} from '../src/output/OutputBuffer.js'; +import {selectedText} from '../src/output/TranscriptSelection.js'; +import {displayWidth} from '../src/util/text.js'; + +const CONFIRMATION = 'Copied to clipboard · 113 characters · 5 lines'; +const LIFECYCLE = '✔ Completed · 1m 25s · 18s waiting for input (2 prompts) · 15:42'; + +const all = (rows: readonly unknown[]) => ({anchor: 0, head: rows.length - 1, dragging: false, moved: true, pointerY: 0}); + +test('NMSh confirmations wrap at words with a hanging indent, never mid-word', () => { + const output = new OutputBuffer(); + output.addFrontendInteraction('/copy', CONFIRMATION); + const rows = output.wrapped(46).map(row => row.plain); + assert.deepEqual(rows, ['❯ /copy', ' ⎿ Copied to clipboard · 113 characters · 5 ', ' lines']); + assert.deepEqual(output.wrapped(30).map(row => row.plain), ['❯ /copy', ' ⎿ Copied to clipboard · 113 ', ' characters · 5 lines']); +}); + +test('word-wrapped NMSh lines stay exact for selection and fit the width at every size', () => { + for (let width = 6; width <= 70; width += 1) { + const output = new OutputBuffer(); + output.addFrontendInteraction('/copy', CONFIRMATION); + output.addHistoryLine(LIFECYCLE); + output.addHistoryLine(' ⎿ 漢字の通知 · wide characters 漢字 stay whole'); + const rows = output.wrapped(width); + for (const row of rows) assert.ok(displayWidth(row.plain) <= width, `width ${width}: "${row.plain}"`); + assert.equal(selectedText(rows, all(rows)), + ['❯ /copy', ` ⎿ ${CONFIRMATION}`, '', LIFECYCLE, '', ' ⎿ 漢字の通知 · wide characters 漢字 stay whole'].join('\n'), `width ${width}`); + } +}); + +test('shell output keeps terminal column wrapping', () => { + const output = new OutputBuffer(); + output.beginCommand('echo', ['❯ echo']); + output.write(` ⎿ ${CONFIRMATION}\r\n`); + output.complete(0); + const rows = output.wrapped(28).map(row => row.plain); + assert.deepEqual(rows.slice(1), [' ⎿ Copied to clipboard · 11', '3 characters · 5 lines']); +}); diff --git a/tests/startupDiscovery.test.ts b/tests/startupDiscovery.test.ts index ab39cf43..fe834422 100644 --- a/tests/startupDiscovery.test.ts +++ b/tests/startupDiscovery.test.ts @@ -167,7 +167,7 @@ test('several detached sessions: pick some, Esc for none; this window owns one, picker.pty.write('\r'); await picker.waitFor(/Reattached live session/); assert.equal(await shellPid(picker), pids[2], 'this window takes the first selected'); - await picker.waitFor(/1 more selected live session need their own windows[\s\S]*they keep running/); + await picker.waitFor(/1\s+more\s+selected\s+live\s+session\s+need\s+their\s+own\s+windows[\s\S]*they\s+keep\s+running/); await detachedCount(sandbox, 2); assert.ok(pids.every(pid => processAlive(pid)), 'nothing selected or unselected was ended'); } finally { @@ -184,7 +184,7 @@ test('Open all restores every detached session: one here, the others as far as t assert.doesNotMatch(strip(all.output), /detached live sessions/, 'no picker'); assert.equal(await shellPid(all), pids[1]); // The test host cannot open windows: the other session is named with its attach command. - await all.waitFor(/nmsh --attach [\w-]+/); + await all.waitFor(/nmsh\s+--attach\s+[\w-]+/); await detachedCount(sandbox, 1); } finally { await sandbox.dispose(); From 68ea9d729da72b9913cd8efaac822e477b6b3a87 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:21:50 +0530 Subject: [PATCH 020/249] Live test: a timed read is 'probably waiting' on Linux too zsh and bash 'read -t' wait in select/pselect6/ppoll, not read(), so the kernel's report never confirms them; the discard test now accepts either certainty, and the syscall test pins timed reads as waits. --- tests/inputAwareness.test.ts | 3 +++ tests/inputAwarenessLive.test.ts | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/inputAwareness.test.ts b/tests/inputAwareness.test.ts index c6065dae..75f88e83 100644 --- a/tests/inputAwareness.test.ts +++ b/tests/inputAwareness.test.ts @@ -87,6 +87,9 @@ test('Linux syscall report: a read of a descriptor, a poll/select wait, somethin assert.deepEqual(parseSyscall('63 0x3 0xffff 0x1 0 0 0 0xff 0xff', 'arm64'), {kind: 'read', fd: 3}); assert.deepEqual(parseSyscall('7 0x7ffd 0x1 0xffffffff 0 0 0 0x1 0x2', 'x64'), {kind: 'wait'}); assert.deepEqual(parseSyscall('230 0x0 0x0 0x7ffd 0 0 0 0x1 0x2', 'x64'), {kind: 'other'}, 'clock_nanosleep: sleeping, not reading'); + // A timed read (zsh `read -t`, bash `read -t`) waits in select/pselect6/ppoll: a wait, never a confirmed read. + for (const nr of [23, 270, 271]) assert.deepEqual(parseSyscall(`${nr} 0x1 0x7ffd 0x0 0x0 0x7ffd 0x0 0x1 0x2`, 'x64'), {kind: 'wait'}); + assert.deepEqual(parseSyscall('73 0x7ffd 0x1 0x7ffd 0x0 0x8 0x0 0x1 0x2', 'arm64'), {kind: 'wait'}, 'ppoll'); assert.equal(parseSyscall('running', 'x64'), undefined); assert.equal(parseSyscall('-1 0x7ffd 0x1', 'x64'), undefined); }); diff --git a/tests/inputAwarenessLive.test.ts b/tests/inputAwarenessLive.test.ts index 98e6ca22..cdda7d62 100644 --- a/tests/inputAwarenessLive.test.ts +++ b/tests/inputAwarenessLive.test.ts @@ -227,7 +227,8 @@ test('live: a reply typed for a command that finishes first is discarded, never const marker = join(sandbox.home, 'must-not-exist'); const mark = frontend.mark; frontend.pty.write(`read -t 4 'x?Quick? ' || echo TIMED-OUT\r`); - await frontend.waitFor(new RegExp(LINE_WAIT, 'u'), mark); + // A timed read waits in poll/select, not read(), so even Linux's kernel evidence leaves it "probably". + await frontend.waitFor(/[◆◇] (?:Probably w|W)aiting for input/u, mark); frontend.pty.write(`touch ${marker}`); await frontend.waitFor(/reply was discarded, not run/u, mark, 20_000); frontend.pty.write('\r'); From c4c891e5f6d9db06ab96aae832fc322ab03a4ad0 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:31:45 +0530 Subject: [PATCH 021/249] Input awareness: a lapsed prompt resumes as the same wait, with its own start When the evidence for a waiting program lapsed for a look (a slow probe, a resize while a window detaches) and returned with nothing written or typed in between, the watcher reopened it as a new wait starting at the lapse. Other windows and /sessions then showed a fresh wait, and macOS CI saw the start move across a detach. Now the same prompt keeps its start, and the time that closing it added is taken back, so it is still counted once. --- src/session/InputWatch.ts | 23 +++++++++++++++++++---- tests/inputAwareness.test.ts | 5 +++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/session/InputWatch.ts b/src/session/InputWatch.ts index a454f25f..019d75a7 100644 --- a/src/session/InputWatch.ts +++ b/src/session/InputWatch.ts @@ -76,6 +76,11 @@ export class InputWatch extends EventEmitter { private countedEpoch = -1; /** When the last wait ended: a new wait never starts earlier, so no time is counted twice. */ private lastWaitEnd = 0; + /** + * The wait that just closed without output or input (evidence lapsed), and what closing it added. If the evidence + * returns in the same epoch it is the same prompt: it resumes with its own start, and that time is taken back. + */ + private lapsed?: {request: InputRequest; epoch: number; addedMs: number; previousEnd: number}; private lastActivity = 0; private lastOutput = 0; /** Bumped by every stream event; a probe that started before the latest event is stale. */ @@ -158,11 +163,14 @@ export class InputWatch extends EventEmitter { this.removeAllListeners(); } - private closeWait(at: number, emit: boolean): void { + private closeWait(at: number, emit: boolean, lapse = false): void { const request = this.request; + this.lapsed = undefined; if (!request) return; this.request = undefined; - this.waitedMs += Math.max(0, at - request.since); + const addedMs = Math.max(0, at - request.since); + if (lapse) this.lapsed = {request, epoch: this.epoch, addedMs, previousEnd: this.lastWaitEnd}; + this.waitedMs += addedMs; this.lastWaitEnd = Math.max(this.lastWaitEnd, at); if (this.requestEpoch !== this.countedEpoch) { this.waits += 1; this.countedEpoch = this.requestEpoch; } if (emit) this.emit('change', this.state); @@ -215,11 +223,18 @@ export class InputWatch extends EventEmitter { if (!next) { // The evidence went away without input or output (a timed-out read, a restored terminal): the wait ended no // later than now. - if (current) this.closeWait(now, true); + if (current) this.closeWait(now, true, true); return; } + // The same prompt after a lapse (nothing was written or typed since): it keeps its start, counted once. + const resumed = !current && this.lapsed && this.lapsed.epoch === this.epoch ? this.lapsed : undefined; + if (resumed) { + this.waitedMs = Math.max(0, this.waitedMs - resumed.addedMs); + this.lastWaitEnd = resumed.previousEnd; + } + this.lapsed = undefined; const program = sample.foreground.find(item => item.command)?.command; - const request: InputRequest = {since: current?.since ?? Math.max(this.running.since, this.lastOutput, this.lastWaitEnd), confidence: next.confidence, mode: next.mode, + const request: InputRequest = {since: current?.since ?? resumed?.request.since ?? Math.max(this.running.since, this.lastOutput, this.lastWaitEnd), confidence: next.confidence, mode: next.mode, ...(promptText(this.line.line) ? {prompt: promptText(this.line.line)} : {}), ...(program ? {program: program.slice(0, 64)} : {})}; if (current && current.confidence === request.confidence && current.mode === request.mode && current.prompt === request.prompt && current.program === request.program) return; diff --git a/tests/inputAwareness.test.ts b/tests/inputAwareness.test.ts index 75f88e83..a68f8096 100644 --- a/tests/inputAwareness.test.ts +++ b/tests/inputAwareness.test.ts @@ -316,12 +316,13 @@ test('watch: a prompt whose evidence lapses for a moment (a resize waking the pr h.set({modes: keyModes, foreground: fg('R+', 40, 'python3')}); await h.advance(3000); assert.equal(h.watch.state.request, undefined); - const lapsed = h.watch.state.timing.waitedMs; + assert.ok(h.watch.state.timing.waitedMs > 0, 'closing counted the time so far'); h.set({modes: keyModes, foreground: fg('S+', 40, 'python3')}); await h.advance(3000); const reopened = h.watch.state.request; assert.ok(reopened); - assert.ok(reopened.since >= asked + lapsed, 'the reopened wait starts where the last one ended, never at the old prompt'); + assert.equal(reopened.since, asked, 'the same prompt: it keeps its own start (what other windows and /sessions show)'); + assert.equal(h.watch.state.timing.waitedMs, 0, 'resuming took that back: the open wait counts it once, when it ends'); h.watch.onInput('y', h.at()); h.watch.onOutput('yes\r\nSecond question. Fix? ', h.at()); h.set({modes: keyModes, foreground: fg('S+', 41, 'python3')}); From b302cd9219345e1462d57e20518521808c02fa8d Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:18:27 +0530 Subject: [PATCH 022/249] Command queue: prepare the next commands while one runs; they run in order in the same shell While a command runs, Enter adds the composer's command to the session's queue (Ctrl+Q always queues, Ctrl+S sends the line to the running program). Entries run one at a time through the session's own shell, each only after the previous command reached its prompt, so directory, variables, functions and options carry over. A failure, Ctrl+C or a shell switch pauses the rest with the reason; /queue resume continues and /queue clear drops them, and nothing runs twice. - The queue belongs to the session service (in-process client alike): it keeps running while no window is attached, and the exec event says which command came from the queue, so a window that attaches later shows "from queue" for it too. - Input ownership first: a program known to wait for input, or one that left a question open on its line ("Password: ", "Continue? [y/N] ") before any probe looked, gets Enter; a reply is never queued or run as a command. After Ctrl+C or Ctrl+Z, typing goes to the shell prompt as type-ahead. - Ctrl+Q during single-key input composes an entry the program never sees; Enter adds it. - /queue opens a manager to edit, reorder, remove, pause and clear; the queue line by the composer shows what runs next or why it waits. Settings, Setup, Ask and settings export know about it. - Live tests over zsh, Bash and Fish: ordering and state, failure and interrupt pauses, prompts, hidden replies, multi-line entries, detach and reattach, narrow/NO_COLOR/Safe glyphs. A live start that never becomes ready now ends its sandbox instead of hanging the test file. --- src/app/TerminalApp.ts | 291 ++++++++++++++++++++++---- src/ask/concepts.ts | 5 +- src/commands/slashCommands.ts | 4 + src/configuration/portability.ts | 2 +- src/prompt/configuration.ts | 5 +- src/queue/QueuePanel.ts | 89 ++++++++ src/session/CommandQueue.ts | 163 +++++++++++++++ src/session/InProcessSessionClient.ts | 38 +++- src/session/QueueDispatcher.ts | 121 +++++++++++ src/session/SessionClient.ts | 16 ++ src/session/SessionProtocol.ts | 77 ++++++- src/session/SessionService.ts | 39 +++- src/session/SocketSessionClient.ts | 11 +- src/session/StreamBacklog.ts | 2 +- src/session/liveStatus.ts | 7 + src/setup/SetupCat.ts | 1 + src/shell/submission.ts | 8 + src/status/queueStatus.ts | 68 ++++++ src/terminal/keys.ts | 8 +- src/ui/SettingsPanel.ts | 2 + tests/commandQueue.test.ts | 150 +++++++++++++ tests/commandQueueLive.test.ts | 256 ++++++++++++++++++++++ tests/inputAwarenessLive.test.ts | 12 +- tests/sessionProtocol.test.ts | 2 + 24 files changed, 1305 insertions(+), 72 deletions(-) create mode 100644 src/queue/QueuePanel.ts create mode 100644 src/session/CommandQueue.ts create mode 100644 src/session/QueueDispatcher.ts create mode 100644 src/shell/submission.ts create mode 100644 src/status/queueStatus.ts create mode 100644 tests/commandQueue.test.ts create mode 100644 tests/commandQueueLive.test.ts diff --git a/src/app/TerminalApp.ts b/src/app/TerminalApp.ts index bd48858d..83e601f9 100644 --- a/src/app/TerminalApp.ts +++ b/src/app/TerminalApp.ts @@ -1,3 +1,4 @@ +import {shellSubmission} from '../shell/submission.js'; import {railNeedsPromptConversion, railPreviewConfiguration} from '../prompt/railLayout.js'; import {prepareRail, paintRailComposition, railCompositionPreview, type PreparedRail} from '../prompt/railComposition.js'; import {routeModule} from '../context/surfaceRouter.js'; @@ -178,7 +179,12 @@ import {TaskProgress} from '../status/TaskProgress.js'; import {renderStatusStrip, stripStatsFromFacts, stripVisible, type StripModuleItem} from '../status/StatusStrip.js'; import {PATH_DISPLAY_LEVELS} from '../prompt/pathDisplay.js'; import {completedActivity, liveActivityParts} from '../status/activity.js'; +import {OpenLine, promptShaped} from '../session/openLine.js'; import {completionWaitFact, directInputPlaceholder, inputActivityRows, inputGlyph, inputProgram, waitedDetail} from '../status/inputStatus.js'; +import {pauseReason, pausedGlyph, queueGlyph, queueRow} from '../status/queueStatus.js'; +import {queuePanelKey, renderQueuePanel, type QueuePanelState} from '../queue/QueuePanel.js'; +import type {QueueEntry, QueueState} from '../session/CommandQueue.js'; +import type {QueueNews} from '../session/SessionClient.js'; import {directInput, type InputState, type InputTiming} from '../session/inputState.js'; import {directKeyBytes} from '../terminal/keyBytes.js'; import {extractFacts} from '../status/adapters.js'; @@ -706,7 +712,12 @@ export class TerminalApp { /** Terminal modes the running command has set, for handing the terminal to it mid-command. */ private readonly commandModes = new AlternateScreenTracker(); private settingsPanelState?: SettingsPanelState; - private running?: {command: string; startedAt: number; interrupted: boolean; cleared: boolean; startId: number; cwd: string; historyAllowed?: number; awaitingExec?: boolean}; + private running?: {command: string; startedAt: number; interrupted: boolean; cleared: boolean; startId: number; cwd: string; historyAllowed?: number; awaitingExec?: boolean; + /** Started from the command queue rather than typed at the prompt. */ + queued?: boolean; + /** You sent Ctrl+C or Ctrl+Z: the command is ending at your request, so what you type next is for the shell's + * prompt (type-ahead, as in any terminal), not a queue entry held behind the pause that stop causes. */ + stopping?: boolean}; private hoveredLineIndex?: number; /** NMSh-owned transcript selection (plain drag); presentation only. */ private selection?: TranscriptSelection; @@ -732,6 +743,15 @@ export class TerminalApp { * are type-ahead for the shell and stay in the composer. */ private runningDraft?: {startId: number; at: number; answering: boolean}; + /** This session's command queue as its owner (the session service or in-process client) last reported it. */ + private queueState: QueueState = {entries: []}; + /** Ctrl+Q while a program reads keys directly: the composer takes typing for one queue entry, never the program. */ + private queueCompose = false; + /** The line the running command left the cursor on: a question there is answered before any probe confirms it. */ + private readonly commandLine = new OpenLine(); + /** A queued entry being edited in the composer: Enter saves it in place, Esc cancels. */ + private queueEdit?: {id: number; draft: string}; + private queuePanel?: QueuePanelState; /** One pending timeout at a time drives the welcome cat's occasional blink. */ private welcomeBlinkTimer?: () => void; private welcomeBlinkCount = 0; @@ -807,9 +827,10 @@ export class TerminalApp { this.onShellPrompt(marker.exitCode, marker.cwd, stamp.at); } }); - this.session.on('exec', (command, stamp) => { if (this.inStream(stamp)) { this.hostSemantics.exec(); this.onShellExec(command, stamp.at, stamp.historyAllowed); } }); + this.session.on('exec', (command, stamp) => { if (this.inStream(stamp)) { this.hostSemantics.exec(); this.onShellExec(command, stamp.at, stamp.historyAllowed, stamp.queued); } }); this.session.on('replayed', summary => this.finishReplay(summary)); this.session.on('inputState', state => this.onInputState(state)); + this.session.on('queueState', (state, news) => this.onQueueState(state, news)); this.session.on('inputRejected', (data, submission) => this.onInputRejected(data, submission)); this.session.on('startup', tail => { this.startupTail = tail; @@ -956,7 +977,7 @@ export class TerminalApp { `${formatBytes(summary.truncatedBytes)} of output produced while detached exceeded the retention limit and was not kept.`, ERROR); } // Without a journal the running command is known only from the service. - if (!this.running && attached.running) this.onShellExec(attached.running, attached.runningSince); + if (!this.running && attached.running) this.onShellExec(attached.running, attached.runningSince, undefined, attached.runningQueued); if (!this.startupPending && this.running && (attached.fullscreen !== 0 || shouldPassthrough(this.running.command))) { this.cancelPresentation(); this.passthrough = true; @@ -1015,7 +1036,7 @@ export class TerminalApp { * zsh started a command line this frontend did not submit (it began while * detached, or came from type-ahead): give it its own transcript block. */ - private onShellExec(command: string, at = Date.now(), historyAllowed?: number): void { + private onShellExec(command: string, at = Date.now(), historyAllowed?: number, queued?: string): void { this.effects.cancel(); if (this.running) { this.running.awaitingExec = false; @@ -1029,11 +1050,21 @@ export class TerminalApp { return; } this.commandModes.reset(); + this.commandLine.reset(); this.inputState = undefined; + // A queued entry runs under its own text (a multi-line entry reaches the shell wrapped as one block); the session's + // queue says so on the exec itself, so a window that attached later shows it the same way. + if (queued !== undefined) command = queued; + if (!this.startupPending && !this.passthrough && shouldPassthrough(command)) { + this.cancelPresentation(); + this.passthrough = true; + this.noteForeignScreen(); + this.handOverTerminal(); + } const startId = this.output.beginCommand(command, this.formatCommandAnsi(command, null), mode => this.onActiveModeChange(mode), this.historicalContext(this.shellCwd, this.context, command)); this.tapActivityObserver.reset(this.output.activeOutputStartId ?? startId); - this.running = {command, startedAt: at, interrupted: false, cleared: false, startId, cwd: this.shellCwd, historyAllowed}; + this.running = {command, startedAt: at, interrupted: false, cleared: false, startId, cwd: this.shellCwd, historyAllowed, ...(queued !== undefined ? {queued: true} : {})}; this.scheduleJournal(); if (!this.replaying) this.render(); } @@ -1559,6 +1590,14 @@ export class TerminalApp { return; } if (this.cursorPanel) { this.handleCursorPanelKey(key, this.cursorPanel); this.render(); return; } + if (this.queuePanel) { + const action = queuePanelKey(this.queuePanel, key, this.queueState); + if (action?.kind === 'close') { this.queuePanel = undefined; this.returnFromPanel(); } + else if (action?.kind === 'change') this.session.queue(action.change); + else if (action?.kind === 'edit') { this.queuePanel = undefined; this.returnFromPanel(); this.beginQueueEdit(action.id); } + this.render(); + return; + } if (this.watchPanel) { const panel = this.watchPanel; const action = watchPanelKey(panel, key, this.watches.active()); @@ -1835,6 +1874,24 @@ export class TerminalApp { } // A program reading a hidden line or single keys gets each key as it is typed, as in any terminal: the // composer neither shows nor keeps it, and Enter is a key for the program, never a shell submission. + // Ctrl+Q while a program reads keys directly: the composer takes the next command for the queue; the program + // never sees those keys. Esc returns typing to the program. + if (key.kind === 'queueCommand' && this.directInputRequest() && this.queueAvailable()) { + this.queueCompose = true; + this.render(); + return; + } + if (key.kind === 'escape' && this.queueCompose) { + this.queueCompose = false; + this.editor.clear(); + this.render(); + return; + } + if (key.kind === 'escape' && this.queueEdit) { + this.cancelQueueEdit(); + this.render(); + return; + } if (this.directInputRequest()) { this.sendDirectKey(key); return; @@ -1842,7 +1899,7 @@ export class TerminalApp { // Typing that starts while a command waits for a line is an answer: if the command ends before it is sent, it // is discarded rather than left to run as the next shell command. if (this.running && !this.editor.text && (key.kind === 'text' || key.kind === 'paste')) { - this.runningDraft = {startId: this.running.startId, at: Date.now(), answering: Boolean(this.inputState?.request)}; + this.runningDraft = {startId: this.running.startId, at: Date.now(), answering: this.answersByDefault()}; } // Flow keeps the composer in the document: editing while scrolled back // returns to it first. Scrolling and mouse navigation alone never do. @@ -1888,6 +1945,7 @@ export class TerminalApp { this.clearCorrection(); if (this.running) { this.running.interrupted = true; + this.running.stopping = true; this.editor.clear(); this.session.interrupt(); } else { @@ -1907,7 +1965,7 @@ export class TerminalApp { // Job control belongs to zsh: forward ^Z so it stops the foreground job. // With no foreground command there is nothing to suspend, so the idle // composer ignores it rather than treating it as text, undo, or exit. - if (this.running) this.session.write('\u001A'); + if (this.running) { this.running.stopping = true; this.session.write('\u001A'); } return; } if (key.kind === 'selectAll') { @@ -2118,30 +2176,9 @@ export class TerminalApp { else if (key.kind === 'deleteLineAfter') this.editor.deleteLineAfter(); else if (key.kind === 'delete') this.editor.delete(); else if (key.kind === 'newline') this.editor.insert('\n'); - else if (key.kind === 'enter') { - if (this.running) { - if (this.editor.text.trim() === '/zsh') { - this.editor.clear(); - this.output.addFrontendInteraction('/zsh', 'Wait for the foreground command to finish or interrupt it, then run /zsh.', INFO); - } else if (this.editor.text.trim() === '/clear') { - this.editor.clear(); - this.output.addFrontendInteraction('/clear', 'Wait for the foreground command to finish before archiving this transcript.', INFO); - } else if (this.editor.text.trim() === '/resume') { - this.editor.clear(); - this.output.addFrontendInteraction('/resume', 'Wait for the foreground command to finish before switching transcripts.', INFO); - } else { - const input = this.editor.text; - this.editor.clear(); - this.runningDraft = undefined; - this.session.write(`${input}\r`); - return; - } - this.editor.clear(); - } else { - if (this.preparingCommand) return; - void this.submit(); - } - } + else if (key.kind === 'queueCommand') this.routeComposer('queue'); + else if (key.kind === 'sendToProgram') this.routeComposer('send'); + else if (key.kind === 'enter') this.routeComposer('enter'); } @@ -2497,6 +2534,7 @@ export class TerminalApp { else if (slash.kind === 'llm') this.openUnderstandingPanel(); else if (slash.kind === 'doctor') void this.openDoctor(); else if (slash.kind === 'watch') this.handleWatch(command, slash.op, slash.arguments); + else if (slash.kind === 'queue') this.handleQueueCommand(command, slash.op); else if (slash.kind === 'rename') { if (this.sessionMode !== 'service' || !this.sessionId) this.output.addFrontendInteraction(command, 'Renaming needs a live session (this one runs in-process).', INFO); else { @@ -2664,11 +2702,7 @@ export class TerminalApp { this.noteForeignScreen(); this.handOverTerminal(); } - if (command.includes('\n')) { - this.session.submit(`{ ${command}\n}`); - } else { - this.session.submit(command); - } + this.session.submit(shellSubmission(command)); // After the command is on its way: one sweep acknowledging the submission (never delays it). if (!this.passthrough) this.startSweep('prompt', 'vivid'); this.render(); @@ -3117,6 +3151,7 @@ export class TerminalApp { process.stdout.write(data); } else { this.commandModes.observeModes(data); + if (this.running) this.commandLine.push(data); this.lastOutputTime = Date.now(); const wasPassthrough = this.passthrough; this.output.write(data); @@ -3204,6 +3239,7 @@ export class TerminalApp { if (failure) parts.main = parts.main.replace('Command failed', failure === 'command-not-found' ? 'Command not found' : 'Shell syntax error'); else if (agent) parts.main = agentCompletionText(agent.id, elapsed, exitCode, isInterrupted); parts.main += completionWaitFact(this.completedInputTiming); + if (command.queued) parts.main += ' · from queue'; this.output.setCompletionLifecycle(`${parts.main}${parts.detail}`); const rowStyle = isInterrupted ? STOPPED : (exitCode !== 0 ? ERROR : SUCCESS); this.output.addHistoryLine(`${rowStyle}${parts.main}${SECONDARY}${parts.detail}${RESET}`); @@ -3213,6 +3249,7 @@ export class TerminalApp { } this.running = undefined; this.inputState = undefined; + this.queueCompose = false; this.completedInputTiming = undefined; // An answer typed for this command but never sent must not become the next shell command. const draft = this.runningDraft; @@ -3941,7 +3978,7 @@ export class TerminalApp { private get settingsPanelActive(): boolean { return Boolean(this.stopsEditor || this.chromeEditor || this.screensaverPanel || this.themeStudio || this.themeBridgePanel || this.rowPanel || this.stripStudio || this.configureList || this.tmuxPanel || this.dotfiles || this.setupState || this.installPrompt || this.presetPanel || this.toolsPanel || this.toolConfigurationLoading || this.toolConfiguration || this.promptPanelState || this.transcriptPanelState || this.providerPanelState || this.paletteState || this.syntaxPanelState || this.layoutPanelState || this.settingsPanelState - || this.resumeBrowser || this.appearanceHub || this.keyboardState || this.startupPanel || this.aboutPanel || this.shellPanel || this.openPanel || this.askState || this.agentView || this.agentPanel || this.launcher || this.modsPanel || this.worktreePanel || this.githubPanel || this.providersOverview || this.understandingPanel || this.cursorPanel || this.doctorPanel || this.watchPanel || this.pasteReview || this.misePanel || this.keepAwakePanel); + || this.resumeBrowser || this.appearanceHub || this.keyboardState || this.startupPanel || this.aboutPanel || this.shellPanel || this.openPanel || this.askState || this.agentView || this.agentPanel || this.launcher || this.modsPanel || this.worktreePanel || this.githubPanel || this.providersOverview || this.understandingPanel || this.cursorPanel || this.doctorPanel || this.watchPanel || this.queuePanel || this.pasteReview || this.misePanel || this.keepAwakePanel); } /** Complex panels declare the smallest size that shows their essential controls. */ @@ -4035,6 +4072,7 @@ export class TerminalApp { return framePanel(renderAsk(this.askState, columns, {presentation: this.promptConfiguration.askPresentation, height: this.dimensions().rows - 4, shell: this.shellId, ...(activity ? {activity} : {})}), columns); } if (this.pasteReview) return framePanel(renderPasteReview(this.pasteReview, columns, this.dimensions().rows - 4), columns); + if (this.queuePanel) return framePanel(renderQueuePanel(this.queuePanel, this.queueState, this.running?.command, columns, this.dimensions().rows - 4), columns); if (this.watchPanel) return framePanel(renderWatchPanel(this.watchPanel, this.watches.active(), columns, Date.now(), this.dimensions().rows - 4), columns); if (this.doctorPanel) return framePanel(renderDoctorPanel(this.doctorPanel, columns, Date.now(), !this.decorativeMotionAllowed()), columns); if (this.cursorPanel) return framePanel(this.cursorPanelRows(this.cursorPanel, columns, this.cursorEnv(this.cursorPanel.draft, this.promptConfiguration)), columns); @@ -8471,7 +8509,7 @@ export class TerminalApp { inputRows: fullInput.allRows.length, suggestions, inspectorRows: this.inspectorRows(columns).length, - running: Boolean(this.running), + running: Boolean(this.running) || Boolean(this.queueLine(80)), detached: this.historyViewport.detached, hasOutput: transcriptRows > 0, composerPosition: this.promptConfiguration.composerPosition, @@ -8582,7 +8620,12 @@ export class TerminalApp { const direct = this.directInputRequest(); const inputRows = input.rows.map(row => { const prefix = row.prefix.startsWith(GLYPHS.prompt) ? `${ACCENT}${GLYPHS.prompt}${RESET}${row.prefix.slice(GLYPHS.prompt.length)}` : row.prefix; - if (direct) return row === input.rows[0] ? truncateAnsi(`${prefix}${SUBTLE}${directInputPlaceholder(direct, inputProgram(direct, this.running!.command))}${RESET}`, this.inputColumns(columns)) : ''; + if (direct) { + const queueHint = this.queueAvailable() ? ' · Ctrl+Q queues a command' : ''; + return row === input.rows[0] ? truncateAnsi(`${prefix}${SUBTLE}${directInputPlaceholder(direct, inputProgram(direct, this.running!.command))}${queueHint}${RESET}`, this.inputColumns(columns)) : ''; + } + const hint = row === input.rows[0] && !this.editor.text ? this.composerHint() : undefined; + if (hint) return truncateAnsi(`${prefix}${SUBTLE}${hint}${RESET}`, this.inputColumns(columns)); let textStyled = ''; const glyphsInRow = graphemes(row.text); for (let i = 0; i < glyphsInRow.length; i++) { @@ -8916,18 +8959,180 @@ export class TerminalApp { * stays in view however far the transcript scrolled or folded. Nothing extra is reserved and the PTY never resizes. */ private activityRows(plan: ScreenPlan, columns: number): string[] { - if (!this.running) return []; + const queue = this.queueLine(columns); + if (!this.running) return queue ? [queue, ''] : []; const request = this.inputState?.request; // A wait reads top to bottom (headline, then the question) wherever the composer is docked. - if (request) return inputActivityRows({...this.inputState!, request}, this.running.command, this.running.startedAt, this.activityAnimationNow, columns).map(row => truncateAnsi(row, columns)); + if (request) { + const rows = inputActivityRows({...this.inputState!, request}, this.running.command, this.running.startedAt, this.activityAnimationNow, columns); + const count = this.queueAvailable() ? this.queueState.entries.length : 0; + // The question keeps both rows; the queue is a count at the end of the headline, when it fits. + const tail = count ? `${SUBTLE} · ${queueGlyph()} ${count} queued${RESET}` : ''; + if (tail && rows[0] !== undefined && displayWidth(stripAnsi(rows[0])) + displayWidth(stripAnsi(tail)) <= columns) rows[0] += tail; + return rows.map(row => truncateAnsi(row, columns)); + } const activity = truncateAnsi(this.currentActivity(), columns); + // The queue reads after what runs now, in the row the region already reserves. + if (queue) return [activity, queue]; return plan.composerPosition === 'top' ? ['', activity] : [activity, '']; } + /** While a command runs, the empty composer says what Enter will do with what you type. */ + private composerHint(): string | undefined { + if (!this.running || this.passthrough || this.queueEdit || !this.queueAvailable() || this.running.stopping) return undefined; + // The program's question keeps the activity rows, so the composer itself says how compose mode ends. + if (this.queueCompose) return 'Queue a command · Enter adds it · Esc returns to the program'; + const request = this.inputState?.request; + const program = request ? inputProgram(request, this.running.command) : commandWords(this.running.command)[0] ?? 'the program'; + return this.answersByDefault() ? `Enter answers ${program} · Ctrl+Q queues a command` : `Enter queues the next command · Ctrl+S sends to ${program}`; + } + + /** + * Enter goes to the running program: it is known to wait for input, or it left a question open on its line + * ("Password: ", "Continue? [y/N] ") that no probe has looked at yet. A reply typed at a fresh prompt (a password + * most of all) must never become a queued shell command, so the open question wins until it is answered. + */ + private answersByDefault(): boolean { + return Boolean(this.inputState?.request) || (this.commandLine.open && promptShaped(this.commandLine.line)); + } + + /** Queueing works when the setting is on and whatever owns the shell runs a queue (an older service does not). */ + private queueAvailable(): boolean { + return this.promptConfiguration.commandQueue !== false && this.session.features.has('queue'); + } + + /** The queue's line: what is queued and what runs next, or why it waits; an edit in progress says how to finish. */ + private queueLine(columns: number): string | undefined { + if (!this.queueAvailable()) return undefined; + if (this.queueEdit) return truncateAnsi(`${SECONDARY}${queueGlyph()} Editing a queued command${RESET}${SUBTLE} · Enter saves · Esc cancels${RESET}`, columns); + return queueRow(this.queueState, columns); + } + + private onQueueState(state: QueueState, news: QueueNews): void { + const previous = this.queueState; + this.queueState = state; + if (news.refused) this.output.addFrontendInteraction('/queue', news.refused, INFO); + if (news.dropped) this.output.addHistoryLine(`${INFO}${news.dropped} queued command${news.dropped === 1 ? '' : 's'} did not run: the shell ended.${RESET}`); + const paused = state.paused; + // A pause NMSh decided (not the person's own) is a safety event: say it where the failure is. + if (paused && paused.reason !== 'user' && paused.at !== previous.paused?.at && state.entries.length) { + this.output.addHistoryLine(`${STOPPED}${pausedGlyph()} Queue paused: ${pauseReason(paused)}.${RESET}${SECONDARY} ${state.entries.length} waiting · /queue to resume or clear${RESET}`); + } + if (this.queueEdit && !state.entries.some(entry => entry.id === this.queueEdit!.id)) { + this.queueEdit = undefined; + this.output.addFrontendInteraction('/queue', 'That queued command already ran or was removed; your edit is still in the composer.', INFO); + } + this.render(); + } + + /** Add the composer's text to the queue. */ + private enqueue(text: string): void { + if (!this.queueAvailable()) { + this.output.addFrontendInteraction('/queue', this.session.features.has('queue') + ? 'Queueing is off (Settings → Sessions → Queue commands).' : 'This session\'s service is older and has no command queue; restart NMSh\'s session service to use it.', INFO); + this.render(); + return; + } + this.session.queue({op: 'add', text}); + this.sessionSubmissions.push({text, slash: false}); + if (this.sessionSubmissions.length > SESSION_SUBMISSION_LIMIT) this.sessionSubmissions.shift(); + this.editor.clear(); + this.composerHistory.reset(); + this.runningDraft = undefined; + this.queueCompose = false; + this.render(); + } + + /** + * Enter, Ctrl+Q (queue) and Ctrl+S (send to the running program). Input ownership comes first: a program that is + * waiting for input (InputWatch's evidence) gets Enter, and only an explicit Ctrl+Q queues then. Otherwise, while + * a command runs, Enter queues; at idle it queues behind pending entries so nothing jumps the order. + */ + private routeComposer(intent: 'enter' | 'queue' | 'send'): void { + // Composing a queue entry (Ctrl+Q during direct input): Enter adds it, and nothing typed here reaches the program. + if (this.queueCompose) { + if (intent === 'send') return; + intent = 'queue'; + } + const text = this.editor.text; + if (this.queueEdit) { + if (intent === 'send') return; + const edit = this.queueEdit; + if (!text.trim()) { this.output.addFrontendInteraction('/queue', 'A queued command cannot be empty; remove it in /queue instead.', INFO); this.render(); return; } + this.queueEdit = undefined; + this.session.queue({op: 'edit', id: edit.id, text}); + this.editor.replaceText(edit.draft); + this.render(); + return; + } + const slash = isSlashInput(text) ? parseSlashCommand(text) : undefined; + if (this.running) { + if (intent !== 'send' && slash && slash.kind !== 'unknown') { + const refused = ({zsh: 'Wait for the foreground command to finish or interrupt it, then run /zsh.', clear: 'Wait for the foreground command to finish before archiving this transcript.', + resume: 'Wait for the foreground command to finish before switching transcripts.'} as Record)[slash.kind === 'shell' && text.trim() === '/zsh' ? 'zsh' : slash.kind]; + if (refused) { this.editor.clear(); this.output.addFrontendInteraction(text.trim(), refused, INFO); this.render(); return; } + void this.submit(); + return; + } + const toProgram = intent === 'send' || (intent === 'enter' && (!this.queueAvailable() || !text.trim() || this.answersByDefault() || this.passthrough || Boolean(this.running.stopping))); + if (toProgram) { + this.editor.clear(); + this.runningDraft = undefined; + this.session.write(`${text}\r`); + return; + } + if (!text.trim()) return; + this.enqueue(text); + return; + } + if (intent === 'send') return; + if (text.trim() && !slash && this.queueAvailable() && (intent === 'queue' || (this.queueState.entries.length > 0 && !this.queueState.paused))) { + this.enqueue(text); + return; + } + if (this.preparingCommand) return; + void this.submit(); + } + + /** `/queue` opens the manager; `/queue pause|resume|clear` act at once. */ + private handleQueueCommand(command: string, op: 'open' | 'pause' | 'resume' | 'clear'): void { + if (!this.queueAvailable()) { + this.output.addFrontendInteraction(command, this.session.features.has('queue') + ? 'Queueing is off (Settings → Sessions → Queue commands).' : 'This session\'s service is older and has no command queue; restart NMSh\'s session service to use it.', INFO); + return; + } + if (op === 'open') { this.panelOrigin = undefined; this.queuePanel = {selected: 0}; return; } + const count = this.queueState.entries.length; + if (op === 'clear') { + this.session.queue({op: 'clear'}); + this.output.addFrontendInteraction(command, count ? `Cleared ${count} queued command${count === 1 ? '' : 's'}; none of them ran.` : 'The queue was already empty.', INFO); + return; + } + this.session.queue({op}); + this.output.addFrontendInteraction(command, op === 'pause' ? `Queue paused${count ? ` with ${count} waiting` : ''}; /queue resume continues it.` : `Queue resumed${count ? `; ${count} will run in order` : ''}.`, INFO); + } + + /** Load a queued entry into the composer to edit it in place; the queue holds at that entry until it is saved. */ + private beginQueueEdit(id: number): void { + const entry = this.queueState.entries.find(item => item.id === id); + if (!entry) return; + this.queueEdit = {id, draft: this.editor.text}; + this.session.queue({op: 'edit-begin', id}); + this.editor.replaceText(entry.text); + } + + private cancelQueueEdit(): void { + const edit = this.queueEdit; + if (!edit) return; + this.queueEdit = undefined; + this.session.queue({op: 'edit-cancel', id: edit.id}); + this.editor.replaceText(edit.draft); + } + /** Keys go straight to the program (a hidden line or single keys) rather than through the composer. */ private directInputRequest() { const request = this.inputState?.request; - return this.running && !this.passthrough && !this.startupPending && directInput(request) ? request : undefined; + return this.running && !this.passthrough && !this.startupPending && !this.queueCompose && directInput(request) ? request : undefined; } private onInputState(state: InputState): void { @@ -8950,7 +9155,7 @@ export class TerminalApp { /** Forward one key to the waiting program as the bytes a terminal sends; nothing is kept, shown or logged. */ private sendDirectKey(key: Key): void { if (key.kind === 'interrupt') { - if (this.running) this.running.interrupted = true; + if (this.running) { this.running.interrupted = true; this.running.stopping = true; } this.session.interrupt(); return; } diff --git a/src/ask/concepts.ts b/src/ask/concepts.ts index e3b4538a..bde6a551 100644 --- a/src/ask/concepts.ts +++ b/src/ask/concepts.ts @@ -124,6 +124,9 @@ export const CONCEPTS: readonly Concept[] = [ {id: 'leave', label: 'Leave NMSh for an ordinary shell', support: 'actionable', capability: 'shell.leave', covers: ['/zsh', '/fish', '/bash', '/exit'], aliases: ['leave nmsh', 'exit nmsh', 'quit nmsh', 'ordinary shell', 'plain shell', 'regular shell'], description: '/zsh, /fish and /bash leave NMSh for an ordinary shell (the session waits; `nmsh` returns). /exit uses your default shell.'}, + {id: 'queue', label: 'Command queue', support: 'actionable', open: '/queue', covers: ['/queue'], + aliases: ['command queue', 'queue a command', 'queue commands', 'run after this finishes', 'run next'], + description: 'While a command runs, Enter queues the next command (Ctrl+Q always queues; Ctrl+S sends to the program). Entries run in order in the same shell after each finishes; a failure or Ctrl+C pauses the rest. /queue shows, edits, reorders, pauses, resumes or clears them.'}, {id: 'sessions', label: 'Live sessions', support: 'actionable', capability: 'session.list', open: '/sessions', covers: ['/sessions', '/rename', 'settings:Sessions'], aliases: ['live sessions', 'running sessions', 'detached sessions', 'current sessions', 'open sessions', 'other sessions', 'sessions', 'session', 'other windows', 'startup restore'], description: 'Live sessions are NMSh sessions running right now, attached or detached. /sessions switches to or ends one.'}, @@ -341,7 +344,7 @@ export const GUIDE_SECTIONS: readonly GuideSection[] = [ {id: 'shells', title: 'Shells', why: 'One NMSh window can run zsh, Fish or Bash, and you can leave for a plain shell and come back.', concepts: ['shell', 'leave', 'otherShells']}, {id: 'completion', title: 'Completion & suggestions', why: 'Tab completion lists real candidates; ghost suggestions predict the rest of the line.', concepts: ['completion', 'suggestions']}, {id: 'history', title: 'History & transcripts', why: 'Find what you ran and what it printed, now or in earlier sessions.', concepts: ['history', 'picker', 'transcript', 'find', 'copy', 'transcripts']}, - {id: 'sessions', title: 'Sessions', why: 'Sessions keep running when a window closes; reattach, switch or get notified.', concepts: ['sessions', 'agentSessions', 'presets', 'notices', 'commandNotifications', 'panes', 'keepAwake']}, + {id: 'sessions', title: 'Sessions', why: 'Sessions keep running when a window closes; reattach, switch or get notified.', concepts: ['sessions', 'queue', 'agentSessions', 'presets', 'notices', 'commandNotifications', 'panes', 'keepAwake']}, {id: 'ask', title: 'Ask', why: 'Plain-English help that knows NMSh, your commands and this repository, and never runs anything you did not confirm.', concepts: ['ask']}, {id: 'intelligence', title: 'Local intelligence', why: 'An optional local model helps Ask map vague requests to known actions. It runs on this machine, never writes commands, and Ask works fully without it.', concepts: ['understanding'], notes: ['Modes: Off, Auto (used only when the deterministic resolver is unsure) and Always (consulted first). /llm shows the model, runtime and last inference.']}, diff --git a/src/commands/slashCommands.ts b/src/commands/slashCommands.ts index 8940689f..fa0b0f24 100644 --- a/src/commands/slashCommands.ts +++ b/src/commands/slashCommands.ts @@ -102,6 +102,7 @@ const RAW_COMMANDS: readonly SlashCommand[] = [ {name: '/nmsh', insertion: '/nmsh raw ', description: 'Raw provider escape: /nmsh raw claude or codex with native CLI args'}, {name: '/guide', insertion: '/guide', description: 'Interactive guide to everything NMSh can do'}, {name: '/rename', insertion: '/rename ', description: 'Name this live session (display only); /rename alone returns to its familiar signature'}, + {name: '/queue', insertion: '/queue', description: 'Commands queued to run after the current one: view, edit, reorder, pause or clear (/queue pause|resume|clear)'}, {name: '/watch', insertion: '/watch ', description: 'Run a command repeatedly and show what changed (/watch git status · --every 10s · /watch stop|pause|resume)'}, {name: '/doctor', insertion: '/doctor', description: 'Health check: NMSh, shell, project, Git, tools, local model and host (local, read-only)'}, {name: '/llm', insertion: '/llm', description: 'Local Intelligence: the optional local model for Ask and Smart Folding (status, setup, stop, remove)'}, @@ -189,6 +190,7 @@ export type ParsedSlashCommand = | {kind: 'llm'} | {kind: 'doctor'} | {kind: 'watch'; op: 'list' | 'stop' | 'pause' | 'resume' | 'now' | 'start'; arguments: string} + | {kind: 'queue'; op: 'open' | 'pause' | 'resume' | 'clear'} | {kind: 'rename'; name: string} | {kind: 'directories', query: string} | {kind: 'history', query: string} @@ -306,6 +308,8 @@ export function parseSlashCommand(input: string): ParsedSlashCommand | undefined if (family) return {kind: 'providers', family: (family === 'pickers' ? 'picker' : family) as 'prompt' | 'welcome' | 'suggestions' | 'history' | 'picker' | 'navigation'}; if (/^\/(?:llm|localllm)\s*$/u.test(input)) return {kind: 'llm'}; if (/^\/doctor\s*$/u.test(input)) return {kind: 'doctor'}; + const queue = /^\/queue(?:\s+(pause|resume|clear))?\s*$/u.exec(input); + if (queue) return {kind: 'queue', op: (queue[1] ?? 'open') as 'open' | 'pause' | 'resume' | 'clear'}; const watch = /^\/watch(?:\s+(.*))?$/u.exec(input); if (watch) { const rest = (watch[1] ?? '').trim(); diff --git a/src/configuration/portability.ts b/src/configuration/portability.ts index a5d1566e..3d64b1f6 100644 --- a/src/configuration/portability.ts +++ b/src/configuration/portability.ts @@ -32,7 +32,7 @@ export const PORTABLE_CATEGORIES = { idle: ['idleVisuals', 'liveActivity'], notifications: ['notifications', 'sessionNotices'], tools: ['toolUpdateChecks', 'installSuggestions', 'ignoredInstallSuggestions', 'updateMode', 'updateFrequency'], - sessions: ['liveSessionStartup', 'liveSessionMultiple', 'sessionRetention', 'terminalTitle'], + sessions: ['liveSessionStartup', 'liveSessionMultiple', 'sessionRetention', 'terminalTitle', 'commandQueue'], agents: ['agentActivity'], shell: ['shellBackend'], editor: ['openWith'], diff --git a/src/prompt/configuration.ts b/src/prompt/configuration.ts index 3f05f234..9ed4cfcf 100644 --- a/src/prompt/configuration.ts +++ b/src/prompt/configuration.ts @@ -638,6 +638,8 @@ export interface PromptConfiguration { localUnderstanding: LocalUnderstandingSettings; /** Local-only agent CLI activity stats (durations and counts; never content). */ agentActivity: boolean; + /** Enter while a command runs queues the next command (Ctrl+S sends to the program); off: Enter sends to the program. */ + commandQueue: boolean; /** Shell backend for new sessions; /shell switches only the current session unless saved as default. */ shellBackend: ShellId; /** Where /open and /open-diff delegate: the surrounding editor (auto), Zed, VS Code, or $VISUAL/$EDITOR. */ @@ -743,6 +745,7 @@ export const DEFAULT_PROMPT_CONFIGURATION: PromptConfiguration = { askPresentation: 'chat' as const, localUnderstanding: {mode: 'auto', ask: true, folding: false}, agentActivity: true, + commandQueue: true, shellBackend: 'zsh', openWith: 'auto', terminalTitle: 'off', @@ -838,7 +841,7 @@ export function normalizePromptConfiguration(value: unknown): PromptConfiguratio : []; const promptSymbolCustom = normalizeCustomGlyph(value.promptSymbolCustom); const tooling = {motion: normalizeMotion(value.motion), pastePreview: (value.pastePreview === 'always' || value.pastePreview === 'off' ? value.pastePreview : 'smart') as 'smart' | 'always' | 'off', cursor: normalizeCursor(value.cursor), statusStrip: normalizeStatusStrip(value.statusStrip), keepAwake: normalizeKeepAwakePresentation(value.keepAwake), idleVisuals: normalizeIdleVisuals(value.idleVisuals), liveActivity: normalizeLiveActivity(value.liveActivity), uiChrome: normalizeUiChrome(value.uiChrome), - sessionNotices: value.sessionNotices !== false, agentProfiles: normalizeProfiles(value.agentProfiles), agentActivity: value.agentActivity !== false, askRecord: value.askRecord !== false, askPresentation: value.askPresentation === 'normal' ? 'normal' as const : 'chat' as const, + sessionNotices: value.sessionNotices !== false, agentProfiles: normalizeProfiles(value.agentProfiles), agentActivity: value.agentActivity !== false, commandQueue: value.commandQueue !== false, askRecord: value.askRecord !== false, askPresentation: value.askPresentation === 'normal' ? 'normal' as const : 'chat' as const, localUnderstanding: normalizeLocalUnderstanding(value.localUnderstanding), shellBackend: isShellId(value.shellBackend) ? value.shellBackend : 'zsh', openWith: OPEN_WITH_IDS.includes(value.openWith as OpenWith) ? value.openWith as OpenWith : 'auto', diff --git a/src/queue/QueuePanel.ts b/src/queue/QueuePanel.ts new file mode 100644 index 00000000..f838a44d --- /dev/null +++ b/src/queue/QueuePanel.ts @@ -0,0 +1,89 @@ +import type {Key} from '../terminal/keys.js'; +import type {QueueOp, QueueState} from '../session/CommandQueue.js'; +import {renderControls} from '../ui/controls.js'; +import {focusForeground, foreground, UI_COLORS} from '../ui/palette.js'; +import {GLYPHS} from '../ui/glyphs.js'; +import {displayWidth, truncateAnsi, truncateText} from '../util/text.js'; +import {pauseReason, pausedGlyph, queueGlyph, queuePreview} from '../status/queueStatus.js'; + +/** + * `/queue`: the session's command queue as a keyboard-first list. It holds no queue of its own: every change is a + * QueueOp sent to the queue's owner, and the list redraws from the owner's next state, so what you see is what will + * run. + */ +export interface QueuePanelState { + selected: number; + /** Waiting for y/n before clearing everything. */ + confirmClear?: boolean; +} + +export type QueuePanelAction = + | {kind: 'close'} + | {kind: 'change'; change: QueueOp} + /** Load the entry into the composer for editing in place. */ + | {kind: 'edit'; id: number}; + +export function queuePanelKey(panel: QueuePanelState, key: Key, queue: QueueState): QueuePanelAction | undefined { + const entries = queue.entries; + panel.selected = Math.max(0, Math.min(panel.selected, entries.length - 1)); + if (panel.confirmClear) { + panel.confirmClear = false; + if (key.kind === 'text' && key.value.toLowerCase() === 'y') return {kind: 'change', change: {op: 'clear'}}; + return undefined; + } + if (key.kind === 'escape' || key.kind === 'interrupt' || (key.kind === 'text' && key.value === 'q')) return {kind: 'close'}; + const entry = entries[panel.selected]; + if (key.kind === 'up' || key.kind === 'down') { + if (entries.length) panel.selected = (panel.selected + (key.kind === 'up' ? -1 : 1) + entries.length) % entries.length; + return undefined; + } + if (key.kind === 'text' && key.value === 'p') return {kind: 'change', change: {op: queue.paused ? 'resume' : 'pause'}}; + if (!entry) return undefined; + if (key.kind === 'selectUp' || key.kind === 'selectDown') { + const to = Math.max(0, Math.min(entries.length - 1, panel.selected + (key.kind === 'selectUp' ? -1 : 1))); + if (to === panel.selected) return undefined; + panel.selected = to; + return {kind: 'change', change: {op: 'move', id: entry.id, to}}; + } + if (key.kind === 'enter' || (key.kind === 'text' && key.value === 'e')) return {kind: 'edit', id: entry.id}; + if (key.kind === 'delete' || key.kind === 'backspace' || (key.kind === 'text' && key.value === 'd')) return {kind: 'change', change: {op: 'remove', id: entry.id}}; + if (key.kind === 'text' && key.value === 'c') { panel.confirmClear = true; return undefined; } + return undefined; +} + +/** The panel's rows (unframed): what runs now, what runs next, and every queued command in order. */ +export function renderQueuePanel(panel: QueuePanelState, queue: QueueState, running: string | undefined, columns: number, height: number): string[] { + const primary = foreground(UI_COLORS.primary); + const secondary = foreground(UI_COLORS.secondary); + const subtle = foreground(UI_COLORS.subtle); + const accent = foreground(UI_COLORS.accent); + const reset = '\u001b[0m'; + const width = Math.max(10, columns - 4); + const entries = queue.entries; + const status = queue.paused ? `${pausedGlyph()} paused · ${pauseReason(queue.paused)}` : entries.length ? `${queueGlyph()} runs in order, one at a time` : 'empty'; + const out = [` ${primary}Command queue${reset} ${queue.paused ? accent : subtle}${truncateText(status, Math.max(4, width - 15))}${reset}`]; + out.push(` ${subtle}${truncateText(running ? `Running: ${queuePreview(running, width)}` : 'Nothing running', width)}${reset}`, ''); + if (!entries.length) { + out.push(` ${secondary}Nothing queued. While a command runs, type the next one and press Enter (or Ctrl+Q).${reset}`); + } + const listRoom = Math.max(1, height - 12); + const first = Math.max(0, Math.min(panel.selected - Math.floor(listRoom / 2), entries.length - listRoom)); + entries.slice(first, first + listRoom).forEach((entry, offset) => { + const index = first + offset; + const selected = index === panel.selected; + const label = index === 0 && !queue.paused ? 'next' : `${index + 1}`; + const editing = queue.editing === entry.id ? ' (editing)' : ''; + const text = queuePreview(entry.text, Math.max(4, width - displayWidth(label) - displayWidth(editing) - 4)); + out.push(`${selected ? `${accent}${GLYPHS.selection}${reset}` : ' '} ${subtle}${label.padStart(4)}${reset} ${focusForeground(selected)}${text}${reset}${subtle}${editing}${reset}`); + }); + const selected = entries[panel.selected]; + if (selected && selected.text.includes('\n') && height >= out.length + 8) { + const lines = selected.text.split('\n'); + out.push('', ` ${subtle}Runs as one block:${reset}`, ...lines.slice(0, 5).map(line => ` ${secondary}${truncateText(line, width)}${reset}`)); + if (lines.length > 5) out.push(` ${subtle}… ${lines.length - 5} more line${lines.length === 6 ? '' : 's'}${reset}`); + } + out.push(''); + if (panel.confirmClear) out.push(` ${accent}Clear ${entries.length} queued command${entries.length === 1 ? '' : 's'}? y / n${reset}`); + else out.push(renderControls([['↑↓', 'select'], ['Shift+↑↓', 'move'], ['Enter', 'edit'], ['Del', 'remove'], ['p', queue.paused ? 'resume' : 'pause'], ['c', 'clear'], ['Esc', 'close']])); + return out.map(line => truncateAnsi(line, columns)); +} diff --git a/src/session/CommandQueue.ts b/src/session/CommandQueue.ts new file mode 100644 index 00000000..ce670f88 --- /dev/null +++ b/src/session/CommandQueue.ts @@ -0,0 +1,163 @@ +import {EventEmitter} from 'node:events'; + +/** + * Commands prepared while another runs, owned by one session and run by its own shell, one at a time. + * + * The queue is data, never a shell construct: no `;`, `&&` or background jobs. Its owner (the session service, or + * the in-process client) is the only place entries are taken for running (see QueueDispatcher), so an entry runs at + * most once, and only in the shell of the session that owns it. + */ +export interface QueueEntry { + /** Unique within the session, never reused. */ + id: number; + /** Exactly what is submitted to the shell, multi-line included. */ + text: string; + addedAt: number; +} + +/** Why the queue does not run its next entry by itself. */ +export type QueuePause = + | {reason: 'user'; at: number} + | {reason: 'failed'; at: number; command: string; exitCode: number} + | {reason: 'interrupted'; at: number; command: string} + | {reason: 'shell-switched'; at: number} + | {reason: 'restored'; at: number}; + +export interface QueueState { + entries: readonly QueueEntry[]; + paused?: QueuePause; + /** An entry being edited holds the queue at that point until the edit is saved or cancelled. */ + editing?: number; +} + +export type QueueOp = + | {op: 'add'; text: string} + | {op: 'edit'; id: number; text: string} + | {op: 'edit-begin'; id: number} + | {op: 'edit-cancel'; id: number} + | {op: 'remove'; id: number} + | {op: 'move'; id: number; to: number} + | {op: 'clear'} + | {op: 'pause'} + | {op: 'resume'}; + +/** Bounds: a queue is for prepared commands, not bulk data. */ +export const QUEUE_LIMIT = 100; +export const ENTRY_LIMIT = 64 * 1024; +/** All queued text together: keeps every queue snapshot well inside one protocol frame. */ +export const QUEUE_BYTES_LIMIT = 1024 * 1024; + +export class CommandQueue extends EventEmitter<{change: [QueueState]}> { + private entries: QueueEntry[] = []; + private nextId = 1; + private pause?: QueuePause; + private editing?: number; + + constructor(private readonly now: () => number = Date.now) { super(); } + + get state(): QueueState { + return {entries: this.entries.map(entry => ({...entry})), ...(this.pause ? {paused: {...this.pause}} : {}), + ...(this.editing === undefined ? {} : {editing: this.editing})}; + } + + get size(): number { return this.entries.length; } + + /** Applies an operation; returns why it was refused, if it was. */ + apply(change: QueueOp): string | undefined { + switch (change.op) { + case 'add': { + if (!change.text.trim()) return 'Nothing to queue.'; + if (change.text.length > ENTRY_LIMIT) return 'That command is too long to queue.'; + if (this.entries.length >= QUEUE_LIMIT) return `The queue holds at most ${QUEUE_LIMIT} commands.`; + if (this.bytes() + Buffer.byteLength(change.text) > QUEUE_BYTES_LIMIT) return 'The queue is full; run or remove some commands first.'; + this.entries.push({id: this.nextId++, text: change.text, addedAt: this.now()}); + break; + } + case 'edit': { + const entry = this.entries.find(item => item.id === change.id); + if (!entry) return 'That command already ran or was removed.'; + if (!change.text.trim()) return 'A queued command cannot be empty; remove it instead.'; + if (change.text.length > ENTRY_LIMIT) return 'That command is too long to queue.'; + if (this.bytes() - Buffer.byteLength(entry.text) + Buffer.byteLength(change.text) > QUEUE_BYTES_LIMIT) return 'The queue is full; run or remove some commands first.'; + entry.text = change.text; + if (this.editing === change.id) this.editing = undefined; + break; + } + case 'edit-begin': { + if (!this.entries.some(item => item.id === change.id)) return 'That command already ran or was removed.'; + this.editing = change.id; + break; + } + case 'edit-cancel': { + if (this.editing !== change.id) return undefined; + this.editing = undefined; + break; + } + case 'remove': { + const index = this.entries.findIndex(item => item.id === change.id); + if (index === -1) return 'That command already ran or was removed.'; + this.entries.splice(index, 1); + if (this.editing === change.id) this.editing = undefined; + break; + } + case 'move': { + const index = this.entries.findIndex(item => item.id === change.id); + if (index === -1) return 'That command already ran or was removed.'; + const to = Math.max(0, Math.min(this.entries.length - 1, change.to)); + const [entry] = this.entries.splice(index, 1); + this.entries.splice(to, 0, entry!); + break; + } + case 'clear': + this.entries = []; + this.editing = undefined; + this.pause = undefined; + break; + case 'pause': + this.pause = {reason: 'user', at: this.now()}; + break; + case 'resume': + this.pause = undefined; + break; + } + this.changed(); + return undefined; + } + + /** Stops automatic running; entries stay. */ + hold(pause: QueuePause): void { + if (!this.entries.length) return; + this.pause = pause; + this.changed(); + } + + /** + * The next entry, removed from the queue, when it may run now: not paused and not held by an edit. Taking is the + * only way an entry leaves for the shell, so it can never run twice. + */ + take(): QueueEntry | undefined { + if (this.pause || !this.entries.length) return undefined; + if (this.editing === this.entries[0]!.id) return undefined; + const entry = this.entries.shift()!; + this.changed(); + return entry; + } + + /** Everything still queued, removed (the shell ended or the session went away). */ + drain(): QueueEntry[] { + const entries = this.entries; + this.entries = []; + this.editing = undefined; + this.pause = undefined; + if (entries.length) this.changed(); + return entries; + } + + private bytes(): number { + return this.entries.reduce((sum, entry) => sum + Buffer.byteLength(entry.text), 0); + } + + private changed(): void { + this.emit('change', this.state); + } +} diff --git a/src/session/InProcessSessionClient.ts b/src/session/InProcessSessionClient.ts index 768c3582..270e2a98 100644 --- a/src/session/InProcessSessionClient.ts +++ b/src/session/InProcessSessionClient.ts @@ -4,6 +4,8 @@ import {ShellSession} from '../shell/ShellSession.js'; import {knowledgeJobCount, type ShellId} from '../shell/adapters/ShellAdapter.js'; import {shellAdapter} from '../shell/adapters/registry.js'; import {SERVICE_FEATURES, type ServiceFeature} from './SessionProtocol.js'; +import {QueueDispatcher, type QueueShell} from './QueueDispatcher.js'; +import type {QueueOp} from './CommandQueue.js'; import {SESSION_ID_ENV, SESSION_MODE_ENV, type SessionClient, type SessionClientEvents, type SessionOptions} from './SessionClient.js'; type ShellLike = Pick @@ -19,6 +21,8 @@ export class InProcessSessionClient extends EventEmitter im private size: {columns: number; rows: number}; private running = false; private knowledge?: string; + /** The session's command queue, run in this process exactly as the service runs it. */ + private readonly dispatcher: QueueDispatcher; /** This in-process session's NMSH_SESSION_ID (there is no service session id). */ readonly contextId = `local-${randomUUID()}`; @@ -27,23 +31,46 @@ export class InProcessSessionClient extends EventEmitter im this.shellId = options.shell ?? 'zsh'; this.size = {columns: options.columns, rows: options.rows}; this.shell = factory({...options, contextId: this.contextId}); + this.dispatcher = new QueueDispatcher(this.queueShell()); + this.dispatcher.on('state', (state, event) => this.emit('queueState', state, { + ...(event.dispatched ? {dispatched: event.dispatched.id} : {}), ...(event.refused ? {refused: event.refused} : {}), + ...(event.dropped?.length ? {dropped: event.dropped.length} : {})})); this.wire(this.shell); } private wire(shell: ShellLike): void { shell.on('data', data => this.emit('data', data, NO_STAMP)); - shell.on('prompt', marker => { this.running = false; this.knowledge = marker.knowledge; this.emit('prompt', marker, NO_STAMP); }); - shell.on('exec', (command, historyAllowed) => { this.running = true; this.emit('exec', command, historyAllowed === undefined ? NO_STAMP : {historyAllowed}); }); + shell.on('prompt', marker => { + this.running = false; + this.knowledge = marker.knowledge; + this.emit('prompt', marker, NO_STAMP); + this.dispatcher.onPrompt(marker.exitCode); + }); + shell.on('exec', (command, historyAllowed) => { + this.running = true; + const queued = this.dispatcher.onExec(command)?.text; + this.emit('exec', command, historyAllowed === undefined && queued === undefined ? NO_STAMP + : {...(historyAllowed === undefined ? {} : {historyAllowed}), ...(queued === undefined ? {} : {queued})}); + }); shell.on('startup', tail => this.emit('startup', tail)); shell.on('inputRejected', (data, submission) => this.emit('inputRejected', data, submission)); shell.on('inputState', state => this.emit('inputState', state)); - shell.on('exit', event => this.emit('exit', event)); + shell.on('exit', event => { this.dispatcher.onShellExit(); this.emit('exit', event); }); } start(): void {} submit(command: string): void { this.shell.submit(command); } - write(data: string): void { this.shell.write(data); } - interrupt(): void { this.shell.interrupt(); } + write(data: string): void { + if (data.includes('\u0003')) this.dispatcher.onInterrupt(); + this.shell.write(data); + } + interrupt(): void { this.dispatcher.onInterrupt(); this.shell.interrupt(); } + queue(change: QueueOp): void { this.dispatcher.request(change); } + + private queueShell(): QueueShell { + const client = this; + return {submit: command => client.shell.submit(command), get isReady() { return client.shell.isReady ?? true; }}; + } endInput(): void { this.shell.endInput(); } resize(columns: number, rows: number): void { this.size = {columns, rows}; this.shell.resize(columns, rows); } kill(): void { this.shell.kill(); } @@ -66,6 +93,7 @@ export class InProcessSessionClient extends EventEmitter im this.shell = next; this.shellId = shell; this.knowledge = undefined; + this.dispatcher.onShellSwitched(this.queueShell()); this.wire(next); return {shell, pid: next.pid ?? 0}; } diff --git a/src/session/QueueDispatcher.ts b/src/session/QueueDispatcher.ts new file mode 100644 index 00000000..94ece3db --- /dev/null +++ b/src/session/QueueDispatcher.ts @@ -0,0 +1,121 @@ +import {shellSubmission} from '../shell/submission.js'; +import {EventEmitter} from 'node:events'; +import {CommandQueue, type QueueEntry, type QueueOp, type QueueState} from './CommandQueue.js'; + +/** What the dispatcher needs from a shell session (ShellSession implements it). */ +export interface QueueShell { + submit(command: string): void; + readonly isReady: boolean; +} + +export interface QueueEvent { + /** The entry just submitted to the shell (it has left the queue). */ + dispatched?: QueueEntry; + /** Why the last requested change was refused. */ + refused?: string; + /** Entries that will never run because the shell ended. */ + dropped?: readonly QueueEntry[]; +} + +/** + * Runs a session's queue in its own shell, one entry at a time, only on the shell's own evidence: + * - an entry is submitted only while the shell is at its prompt (after its prompt marker, before the next exec); + * - the entry leaves the queue in the same step (`CommandQueue.take`), so it can never be submitted twice; + * - after a command that failed (non-zero status) or was interrupted, the queue pauses with that reason instead of + * running what was prepared to follow it; the person resumes or clears it; + * - a shell switch pauses it (entries were written for the previous shell), and a shell exit drops what is left, + * reporting it, never replaying it elsewhere. + * No timers: silence is never taken as completion. + */ +export class QueueDispatcher extends EventEmitter<{state: [QueueState, QueueEvent]}> { + readonly queue: CommandQueue; + private shell: QueueShell; + /** Between a prompt marker and the next exec, with nothing submitted yet. */ + private atPrompt = false; + /** A command started (exec) since the last prompt. */ + private ran = false; + private lastCommand = ''; + /** Ctrl+C was sent while this command ran. */ + private interrupted = false; + /** Set while taking an entry, so the change is announced once, together with what was dispatched. */ + private taking = false; + /** The entry submitted at the last prompt, until the shell reports the command it started. */ + private sent?: QueueEntry; + + constructor(shell: QueueShell, queue = new CommandQueue()) { + super(); + this.shell = shell; + this.queue = queue; + this.queue.on('change', state => { if (!this.taking) this.emit('state', state, {}); }); + } + + get state(): QueueState { return this.queue.state; } + + /** A change requested by the person; the queue may run at once if the shell is idle. */ + request(change: QueueOp): void { + const refused = this.queue.apply(change); + if (refused) { this.emit('state', this.queue.state, {refused}); return; } + this.dispatch(); + } + + /** The shell started a command; returns the queued entry it is, when it is the one just submitted. */ + onExec(command: string): QueueEntry | undefined { + this.atPrompt = false; + this.ran = true; + this.lastCommand = command; + const entry = this.sent; + this.sent = undefined; + return entry; + } + + onInterrupt(): void { + if (!this.atPrompt) this.interrupted = true; + } + + onPrompt(exitCode: number): void { + // A submission that started no command (a comment, an empty block) is not owed to the next command. + this.sent = undefined; + const finished = this.ran; + this.ran = false; + this.atPrompt = true; + if (finished && this.queue.size) { + const at = Date.now(); + if (this.interrupted || exitCode === 130) this.queue.hold({reason: 'interrupted', at, command: this.lastCommand}); + else if (exitCode !== 0) this.queue.hold({reason: 'failed', at, command: this.lastCommand, exitCode}); + } + this.interrupted = false; + this.dispatch(); + } + + /** The session now runs a different shell: nothing queued for the old one runs until the person resumes. */ + onShellSwitched(shell: QueueShell): void { + this.shell = shell; + this.atPrompt = false; + this.ran = false; + this.sent = undefined; + this.queue.hold({reason: 'shell-switched', at: Date.now()}); + } + + /** The shell ended: what is left is reported and never runs. */ + onShellExit(): void { + this.atPrompt = false; + this.taking = true; + let dropped: QueueEntry[]; + try { dropped = this.queue.drain(); } finally { this.taking = false; } + if (dropped.length) this.emit('state', this.queue.state, {dropped}); + } + + private dispatch(): void { + if (!this.atPrompt || !this.shell.isReady) return; + this.taking = true; + let entry: QueueEntry | undefined; + try { entry = this.queue.take(); } finally { this.taking = false; } + if (!entry) return; + // Leaves the prompt now: a second dispatch cannot happen before this entry's own prompt. + this.atPrompt = false; + this.sent = entry; + this.emit('state', this.queue.state, {dispatched: entry}); + // Exactly as the composer submits it: a multi-line entry runs as one block. + this.shell.submit(shellSubmission(entry.text)); + } +} diff --git a/src/session/SessionClient.ts b/src/session/SessionClient.ts index 871ef1f2..04db71cd 100644 --- a/src/session/SessionClient.ts +++ b/src/session/SessionClient.ts @@ -4,11 +4,21 @@ import type {TranscriptSession} from '../sessions/TranscriptStore.js'; import type {ShellId} from '../shell/adapters/ShellAdapter.js'; import type {ServiceFeature} from './SessionProtocol.js'; import type {InputState} from './inputState.js'; +import type {QueueOp, QueueState} from './CommandQueue.js'; + +/** What happened with a queue change, by entry id (the text is in the previous state). */ +export interface QueueNews { + dispatched?: number; + refused?: string; + dropped?: number; +} /** Position of an event in a service session's stream, when it has one. */ export interface StreamStamp { /** Explicit zsh history eligibility; absent from older services. */ historyAllowed?: number; + /** The command is this queued entry, run by the session's queue (its own text, as the person prepared it). */ + queued?: string; seq?: number; /** When the service observed the event (epoch ms). */ at?: number; @@ -27,6 +37,8 @@ export interface SessionClientEvents { inputRejected: [data: string, submission: boolean]; /** The running command's input state changed (see InputWatch); the one source for every input surface. */ inputState: [InputState]; + /** The session's command queue changed: its entries, and what just ran, was refused or was dropped. */ + queueState: [QueueState, QueueNews]; /** The backlog sent after a reattach has been delivered. */ replayed: [{truncatedBytes: number}]; /** The managed shell ended. */ @@ -63,6 +75,8 @@ export interface SessionClient extends EventEmitter { * event); rejects with a factual reason when switching would lose anything. */ switchShell(shell: ShellId, cwd: string): Promise<{shell: ShellId; pid: number}>; + /** Change this session's command queue (only when `features` has 'queue'). */ + queue(change: QueueOp): void; /** Optional capabilities of whatever owns the shell (the service's welcome, or this build in-process). */ readonly features: ReadonlySet; /** Build of the session service, when it reported one. */ @@ -80,6 +94,8 @@ export interface AttachedSession { modes?: string; running?: string; runningSince?: number; + /** The running command is this queued entry (its own text). */ + runningQueued?: string; /** Journal the previous frontend kept for this session, and how far it got. */ journalId?: string; ackedSeq: number; diff --git a/src/session/SessionProtocol.ts b/src/session/SessionProtocol.ts index f4697066..43ac0d5f 100644 --- a/src/session/SessionProtocol.ts +++ b/src/session/SessionProtocol.ts @@ -1,3 +1,5 @@ +import {QUEUE_LIMIT, type QueueEntry, type QueueOp, type QueuePause, type QueueState} from './CommandQueue.js'; +import type {QueueEvent} from './QueueDispatcher.js'; /** * Versioned wire protocol between an NMSh frontend and whatever owns the * PTY + managed zsh (in-process today, a local session service later). @@ -22,7 +24,7 @@ export const MAX_FRAME_BYTES = 8 * 1024 * 1024; * talking to an older service (same protocol version, still running its live * sessions) degrades factually instead of failing later. */ -export const SERVICE_FEATURES = ['shell-switch', 'shell-backends', 'notices', 'input-state'] as const; +export const SERVICE_FEATURES = ['shell-switch', 'shell-backends', 'notices', 'input-state', 'queue'] as const; export type ServiceFeature = typeof SERVICE_FEATURES[number]; /** @@ -30,7 +32,7 @@ export type ServiceFeature = typeof SERVICE_FEATURES[number]; * values (the input-state message, the "input" notice kind) only to a frontend that listed it, so an older frontend * attached to a newer service never receives a frame it would reject. */ -export const CLIENT_FEATURES = ['input-state'] as const; +export const CLIENT_FEATURES = ['input-state', 'queue'] as const; export type ClientFeature = typeof CLIENT_FEATURES[number]; export function parseClientFeatures(text: string | undefined): Set { @@ -62,6 +64,8 @@ export type ClientMessage = | {type: 'switch-shell'; shell: string; cwd: string} /** Rename a live session (display only; its id never changes). An empty name returns to its signature. */ | {type: 'rename'; sessionId: string; name: string} + /** One change to this session's command queue (QueueOp as JSON); only from frontends that listed 'queue'. */ + | {type: 'queue'; change: string} | {type: 'terminate'}; export type ServerMessage = @@ -76,7 +80,7 @@ export type ServerMessage = /** shell: the backend actually started (absent from older services: zsh). */ | {type: 'created'; sessionId: string; pid: number; shell?: string} | {type: 'shell-switched'; shell: string; pid: number} - | {type: 'attached'; sessionId: string; pid: number; cwd: string; fullscreen: number; modes?: string; running?: string; runningSince?: number; + | {type: 'attached'; sessionId: string; pid: number; cwd: string; fullscreen: number; modes?: string; running?: string; runningSince?: number; runningQueued?: string; journalId?: string; ackedSeq: number; knowledge?: string; shell?: string; /** Present only while the shell has not reached its first prompt: the sanitized, bounded tail of its startup output. */ startup?: string} @@ -93,12 +97,14 @@ export type ServerMessage = * missed exactly once with the original timing. */ | {type: 'output'; data: string; seq?: number; at?: number} - | {type: 'exec'; command: string; seq: number; at: number; historyAllowed?: number} + | {type: 'exec'; command: string; seq: number; at: number; historyAllowed?: number; queued?: string} | {type: 'prompt'; exitCode: number; cwd: string; knowledge?: string; seq?: number; at?: number; inputWaitMs?: number; inputWaits?: number} /** * The running command's input state (see InputWatch), sent on every change to frontends that support it: a * request (since/confidence/mode, with the open prompt and program when known) or none, plus waiting time so far. */ + /** The session's command queue (QueueState as JSON) and what just happened to it. */ + | {type: 'queue-state'; state: string; dispatched?: number; refused?: string; dropped?: number} | {type: 'input-state'; since?: number; confidence?: string; mode?: string; prompt?: string; program?: string; waitedMs: number; waits: number} /** End of the backlog sent after attach. */ | {type: 'replayed'; truncatedBytes: number} @@ -148,6 +154,9 @@ export interface SessionInfo { inputConfidence?: string; inputMode?: string; inputPrompt?: string; + /** Commands queued in this session, and why they wait, if paused (only to frontends that listed 'queue'). */ + queued?: number; + queuePaused?: string; } export type ProtocolMessage = ClientMessage | ServerMessage; @@ -177,11 +186,13 @@ const SHAPES: Record = { ack: {seq: 'int', journalId: 'string'}, kill: {sessionId: 'string'}, dismiss: {sessionId: 'string'}, + queue: {change: 'string'}, + 'queue-state': {state: 'string', dispatched: 'int?', refused: 'string?', dropped: 'int?'}, terminate: {}, welcome: {version: 'int', service: 'string', startupSafety: 'int?', features: 'string?', build: 'string?'}, error: {code: 'string', message: 'string'}, created: {sessionId: 'string', pid: 'int', shell: 'string?'}, - attached: {sessionId: 'string', pid: 'int', cwd: 'string', fullscreen: 'int', modes: 'string?', running: 'string?', runningSince: 'int?', + attached: {sessionId: 'string', pid: 'int', cwd: 'string', fullscreen: 'int', modes: 'string?', running: 'string?', runningSince: 'int?', runningQueued: 'string?', journalId: 'string?', ackedSeq: 'int', knowledge: 'string?', startup: 'string?', shell: 'string?'}, startup: {output: 'string'}, 'input-rejected': {data: 'string', submission: 'int?'}, @@ -189,7 +200,7 @@ const SHAPES: Record = { sessions: {sessions: 'sessions', ended: 'notices?'}, dismissed: {sessionId: 'string'}, output: {data: 'string', seq: 'int?', at: 'int?'}, - exec: {command: 'string', seq: 'int', at: 'int', historyAllowed: 'int?'}, + exec: {command: 'string', seq: 'int', at: 'int', historyAllowed: 'int?', queued: 'string?'}, prompt: {exitCode: 'int', cwd: 'string', knowledge: 'string?', seq: 'int?', at: 'int?', inputWaitMs: 'int?', inputWaits: 'int?'}, 'input-state': {since: 'int?', confidence: 'string?', mode: 'string?', prompt: 'string?', program: 'string?', waitedMs: 'int', waits: 'int'}, replayed: {truncatedBytes: 'int'}, @@ -205,7 +216,7 @@ function isEnv(value: unknown): value is Record { const INFO_SHAPE: Shape = {id: 'string', pid: 'int', state: 'string', cwd: 'string', createdAt: 'int', running: 'string?', runningSince: 'int?', idleSince: 'int?', journalId: 'string?', process: 'string?', fullscreen: 'int?', lastOutputAt: 'int?', title: 'string?', attentionSince: 'int?', lastExit: 'int?', notice: 'notice?', shell: 'string?', signature: 'string?', name: 'string?', - inputSince: 'int?', inputConfidence: 'string?', inputMode: 'string?', inputPrompt: 'string?'}; + inputSince: 'int?', inputConfidence: 'string?', inputMode: 'string?', inputPrompt: 'string?', queued: 'int?', queuePaused: 'string?'}; const NOTICE_SHAPE: Shape = {sessionId: 'string', kind: 'string', at: 'int', program: 'string?', agent: 'string?', exitCode: 'int?', durationMs: 'int?', cwd: 'string?', confidence: 'string?'}; @@ -300,3 +311,55 @@ export function inputStateFrom(message: Extract; + const id = Number.isSafeInteger(value.id) ? value.id as number : undefined; + const text_ = typeof value.text === 'string' ? value.text : undefined; + switch (value.op) { + case 'add': return text_ === undefined ? undefined : {op: 'add', text: text_}; + case 'edit': return id === undefined || text_ === undefined ? undefined : {op: 'edit', id, text: text_}; + case 'edit-begin': case 'edit-cancel': case 'remove': return id === undefined ? undefined : {op: value.op, id}; + case 'move': return id === undefined || !Number.isSafeInteger(value.to) ? undefined : {op: 'move', id, to: value.to as number}; + case 'clear': case 'pause': case 'resume': return {op: value.op}; + default: return undefined; + } +} + +const PAUSE_REASONS = ['user', 'failed', 'interrupted', 'shell-switched', 'restored'] as const; + +/** The queue a queue-state message describes; anything malformed is dropped, never guessed. */ +export function queueStateFrom(text: string): QueueState | undefined { + let raw: unknown; + try { raw = JSON.parse(text); } catch { return undefined; } + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined; + const value = raw as Record; + if (!Array.isArray(value.entries) || value.entries.length > QUEUE_LIMIT) return undefined; + const entries: QueueEntry[] = []; + for (const entry of value.entries as unknown[]) { + const item = entry as Record | null; + if (!item || !Number.isSafeInteger(item.id) || typeof item.text !== 'string' || !Number.isSafeInteger(item.addedAt)) return undefined; + entries.push({id: item.id as number, text: item.text, addedAt: item.addedAt as number}); + } + const pause = value.paused as Record | undefined; + let paused: QueuePause | undefined; + if (pause && typeof pause === 'object' && (PAUSE_REASONS as readonly unknown[]).includes(pause.reason) && Number.isSafeInteger(pause.at)) { + const at = pause.at as number; + const command = typeof pause.command === 'string' ? pause.command.slice(0, 400) : ''; + paused = pause.reason === 'failed' ? {reason: 'failed', at, command, exitCode: Number.isSafeInteger(pause.exitCode) ? pause.exitCode as number : 1} + : pause.reason === 'interrupted' ? {reason: 'interrupted', at, command} + : {reason: pause.reason as 'user' | 'shell-switched' | 'restored', at}; + } + const editing = Number.isSafeInteger(value.editing) ? value.editing as number : undefined; + return {entries, ...(paused ? {paused} : {}), ...(editing === undefined ? {} : {editing})}; +} + +/** The queue-state message for a state and its event. */ +export function queueStateMessage(state: QueueState, event: QueueEvent): Extract { + return {type: 'queue-state', state: JSON.stringify(state), ...(event.dispatched ? {dispatched: event.dispatched.id} : {}), + ...(event.refused ? {refused: event.refused.slice(0, 200)} : {}), ...(event.dropped?.length ? {dropped: event.dropped.length} : {})}; +} diff --git a/src/session/SessionService.ts b/src/session/SessionService.ts index 24ed410a..2dfc0031 100644 --- a/src/session/SessionService.ts +++ b/src/session/SessionService.ts @@ -1,3 +1,4 @@ +import {QueueDispatcher} from './QueueDispatcher.js'; import {assignSignature} from './signatures.js'; import {randomUUID} from 'node:crypto'; import {chmodSync, lstatSync, unlinkSync} from 'node:fs'; @@ -6,7 +7,7 @@ import {ShellSession} from '../shell/ShellSession.js'; import {isShellId, knowledgeJobCount, type ShellId} from '../shell/adapters/ShellAdapter.js'; import {shellAdapter} from '../shell/adapters/registry.js'; import {SessionEvidence} from './SessionEvidence.js'; -import {FrameDecoder, PROTOCOL_VERSION, SERVICE_FEATURES, encodeMessage, inputStateMessage, parseClientFeatures, type ClientFeature, type ServerMessage, +import {FrameDecoder, PROTOCOL_VERSION, SERVICE_FEATURES, encodeMessage, inputStateMessage, parseClientFeatures, queueOpFrom, queueStateMessage, type ClientFeature, type ServerMessage, type SessionInfo, type SessionState} from './SessionProtocol.js'; import type {InputState} from './inputState.js'; import {SESSION_ID_ENV, SESSION_MODE_ENV} from './SessionClient.js'; @@ -47,7 +48,7 @@ interface ManagedSession { input?: InputState; /** Pending check that a wait lasted long enough to become a cross-session notice. */ inputNoticeTimer?: NodeJS.Timeout; - running?: {command: string; since: number}; + running?: {command: string; since: number; queued?: string}; /** When zsh last returned to its prompt. */ idleSince: number; screen: AlternateScreenTracker; @@ -60,6 +61,8 @@ interface ManagedSession { /** This session's cross-session notice; cleared when the session is focused. */ notices: SessionNoticeTracker; knowledge?: string; + /** This session's command queue; it runs only in this session's shell (see QueueDispatcher). */ + queue: QueueDispatcher; } export {AlternateScreenTracker} from './TerminalModes.js'; @@ -85,7 +88,7 @@ function toMessage(event: BacklogEvent): ServerMessage { switch (event.kind) { case 'output': return {type: 'output', data: event.data, seq: event.seq, at: event.at}; case 'exec': return {type: 'exec', command: event.command, seq: event.seq, at: event.at, - ...(event.historyAllowed === undefined ? {} : {historyAllowed: event.historyAllowed})}; + ...(event.historyAllowed === undefined ? {} : {historyAllowed: event.historyAllowed}), ...(event.queued === undefined ? {} : {queued: event.queued})}; case 'prompt': return {type: 'prompt', exitCode: event.exitCode, cwd: event.cwd, seq: event.seq, at: event.at, ...(event.knowledge === undefined ? {} : {knowledge: event.knowledge}), ...(event.inputWaits ? {inputWaitMs: event.inputWaitMs ?? 0, inputWaits: event.inputWaits} : {})}; @@ -152,7 +155,9 @@ export class SessionService { ...(evidence.lastExit !== undefined && !running ? {lastExit: evidence.lastExit} : {}), ...(notice ? {notice} : {}), ...(request ? {inputSince: request.since, inputConfidence: request.confidence, inputMode: request.mode, - ...(request.prompt ? {inputPrompt: request.prompt.slice(0, 120)} : {})} : {})}; + ...(request.prompt ? {inputPrompt: request.prompt.slice(0, 120)} : {})} : {}), + ...(features.has('queue') && session.queue.queue.size ? {queued: session.queue.queue.size, + ...(session.queue.state.paused ? {queuePaused: session.queue.state.paused.reason} : {})} : {})}; } async start(): Promise { @@ -242,6 +247,7 @@ export class SessionService { send({type: 'attached', sessionId: info.id, pid: info.pid, cwd: info.cwd, fullscreen: session.screen.ownsTerminal ? 1 : 0, ...(session.screen.ownsTerminal && session.screen.restoreSequence() ? {modes: session.screen.restoreSequence()} : {}), ...(info.running ? {running: info.running, runningSince: info.runningSince} : {}), + ...(features.has('queue') && session.running?.queued !== undefined ? {runningQueued: session.running.queued} : {}), ...(backlog.journalId ? {journalId: backlog.journalId} : {}), ackedSeq: backlog.ackedSeq, ...(session.knowledge === undefined ? {} : {knowledge: session.knowledge}), shell: session.backend, ...(session.shell.isReady ? {} : {startup: session.shell.startupTail() ?? ''})}); @@ -251,6 +257,7 @@ export class SessionService { send({type: 'replayed', truncatedBytes: backlog.truncatedBytes}); // The live input state is not a stream event: a reattaching frontend gets it now, after the replay. if (features.has('input-state') && info.running && session.input) send(inputStateMessage(session.input)); + if (features.has('queue')) send(queueStateMessage(session.queue.state, {})); this.redraw(session, message.columns, message.rows); break; } @@ -270,7 +277,16 @@ export class SessionService { this.endedNotices.dismiss(message.sessionId); send({type: 'dismissed', sessionId: message.sessionId}); break; + case 'queue': { + const change = queueOpFrom(message.change); + if (!owned || !features.has('queue')) send({type: 'error', code: 'state', message: 'no session queue for this connection'}); + else if (!change) send({type: 'error', code: 'queue', message: 'not a queue change'}); + else owned.queue.request(change); + break; + } case 'input': + // Ctrl+C reaching a running command: what was queued after it waits (see QueueDispatcher). + if (message.data.includes('\u0003')) owned?.queue.onInterrupt(); owned?.evidence.onInput(); owned?.notices.clear(); if (owned) { @@ -402,7 +418,10 @@ export class SessionService { state: 'attached', protocolVersion: PROTOCOL_VERSION, signature}; const session: ManagedSession = {record, shell, backend, env, size: {columns, rows}, controller: send, idleSince: Date.now(), screen: new AlternateScreenTracker(), resizes: 0, seq: 0, backlog: new StreamBacklog(spoolPathFor(this.options.runtimeDir, record.id), this.options.backlogLimits), - evidence: new SessionEvidence(), notices: new SessionNoticeTracker(record.id)}; + evidence: new SessionEvidence(), notices: new SessionNoticeTracker(record.id), queue: new QueueDispatcher(shell)}; + session.queue.on('state', (state, event) => { + if (session.controllerFeatures?.has('queue')) session.controller?.(queueStateMessage(state, event)); + }); this.sessions.set(record.id, session); this.wire(session); return session; @@ -427,10 +446,12 @@ export class SessionService { shell.on('inputState', state => this.onInputState(session, state)); shell.on('exec', (command, historyAllowed) => { const at = Date.now(); - session.running = {command, since: at}; + // The queue says whether this is the entry it just submitted: every window, live or later, learns it from here. + const queued = session.queue.onExec(command)?.text; + session.running = {command, since: at, ...(queued === undefined ? {} : {queued})}; session.evidence.onExec(); session.notices.onExec(command, at); - emit({kind: 'exec', seq: ++session.seq, at, command, ...(historyAllowed === undefined ? {} : {historyAllowed})}); + emit({kind: 'exec', seq: ++session.seq, at, command, ...(historyAllowed === undefined ? {} : {historyAllowed}), ...(queued === undefined ? {} : {queued})}); }); shell.on('prompt', marker => { session.knowledge = marker.knowledge; @@ -443,8 +464,11 @@ export class SessionService { emit({kind: 'prompt', seq: ++session.seq, at: Date.now(), exitCode: marker.exitCode, cwd: marker.cwd, ...(marker.knowledge === undefined ? {} : {knowledge: marker.knowledge}), ...(marker.inputWaits ? {inputWaitMs: marker.inputWaitMs ?? 0, inputWaits: marker.inputWaits} : {})}); + // After the prompt is on its way: the next queued command, if any, starts from this evidence only. + session.queue.onPrompt(marker.exitCode); }); shell.on('exit', event => { + session.queue.onShellExit(); this.sessions.delete(record.id); if (session.inputNoticeTimer) { clearTimeout(session.inputNoticeTimer); session.inputNoticeTimer = undefined; } // Ending with no window attached is news; with one attached, that window saw it. @@ -496,6 +520,7 @@ export class SessionService { session.running = undefined; session.idleSince = Date.now(); session.screen.reset(); + session.queue.onShellSwitched(next); this.wire(session); } diff --git a/src/session/SocketSessionClient.ts b/src/session/SocketSessionClient.ts index 3e13f8d3..c7d0fc4c 100644 --- a/src/session/SocketSessionClient.ts +++ b/src/session/SocketSessionClient.ts @@ -1,6 +1,7 @@ import {EventEmitter} from 'node:events'; import {connect, type Socket} from 'node:net'; -import {CLIENT_FEATURES, FrameDecoder, PROTOCOL_VERSION, encodeMessage, inputStateFrom, parseFeatures, type ClientMessage, type ServiceFeature, type ServerMessage} from './SessionProtocol.js'; +import type {QueueOp} from './CommandQueue.js'; +import {CLIENT_FEATURES, FrameDecoder, PROTOCOL_VERSION, encodeMessage, inputStateFrom, parseFeatures, queueStateFrom, type ClientMessage, type ServiceFeature, type ServerMessage} from './SessionProtocol.js'; import type {SessionInfo} from './SessionProtocol.js'; import type {SessionNotice} from './SessionNotices.js'; import {isShellId, type ShellId} from '../shell/adapters/ShellAdapter.js'; @@ -236,7 +237,12 @@ export class SocketSessionClient extends EventEmitter imple ...(message.knowledge === undefined ? {} : {knowledge: message.knowledge}), ...(message.inputWaits ? {inputWaitMs: message.inputWaitMs ?? 0, inputWaits: message.inputWaits} : {})}, {seq: message.seq, at: message.at}); else if (message.type === 'input-state') this.emit('inputState', inputStateFrom(message)); - else if (message.type === 'exec') this.emit('exec', message.command, {seq: message.seq, at: message.at, historyAllowed: message.historyAllowed}); + else if (message.type === 'queue-state') { + const state = queueStateFrom(message.state); + if (state) this.emit('queueState', state, {...(message.dispatched === undefined ? {} : {dispatched: message.dispatched}), + ...(message.refused ? {refused: message.refused} : {}), ...(message.dropped ? {dropped: message.dropped} : {})}); + } + else if (message.type === 'exec') this.emit('exec', message.command, {seq: message.seq, at: message.at, historyAllowed: message.historyAllowed, ...(message.queued === undefined ? {} : {queued: message.queued})}); else if (message.type === 'input-rejected') this.emit('inputRejected', message.data, message.submission === 1); else if (message.type === 'startup') this.emit('startup', message.output); else if (message.type === 'replayed') this.emit('replayed', {truncatedBytes: message.truncatedBytes}); @@ -261,6 +267,7 @@ export class SocketSessionClient extends EventEmitter imple submit(command: string): void { this.send({type: 'input', data: `${command}\r`, submission: 1}); } write(data: string): void { this.send({type: 'input', data}); } + queue(change: QueueOp): void { if (this.features.has('queue')) this.send({type: 'queue', change: JSON.stringify(change)}); } interrupt(): void { this.send({type: 'input', data: '\u0003'}); } endInput(): void { this.send({type: 'input', data: '\u0004'}); } resize(columns: number, rows: number): void { this.send({type: 'resize', columns: Math.max(2, columns), rows: Math.max(2, rows)}); } diff --git a/src/session/StreamBacklog.ts b/src/session/StreamBacklog.ts index d936184d..d8874db2 100644 --- a/src/session/StreamBacklog.ts +++ b/src/session/StreamBacklog.ts @@ -5,7 +5,7 @@ import {MAX_SHELL_KNOWLEDGE_BYTES} from '../shell/ShellKnowledge.js'; /** One shell stream event as retained for replay. */ export type BacklogEvent = | {kind: 'output'; seq: number; at: number; data: string} - | {kind: 'exec'; seq: number; at: number; command: string; historyAllowed?: number} + | {kind: 'exec'; seq: number; at: number; command: string; historyAllowed?: number; queued?: string} | {kind: 'prompt'; seq: number; at: number; exitCode: number; cwd: string; knowledge?: string; inputWaitMs?: number; inputWaits?: number}; /** Non-event spool records: journal acknowledgements, truncation and the shell's end. */ diff --git a/src/session/liveStatus.ts b/src/session/liveStatus.ts index 8cf6448a..acc742ec 100644 --- a/src/session/liveStatus.ts +++ b/src/session/liveStatus.ts @@ -40,6 +40,7 @@ export function liveStatusParts(session: SessionInfo, now: number): string[] { if (!session.running) { const parts = [`idle${session.idleSince ? ` ${formatAge(now - session.idleSince)}` : ''}`]; if (session.lastExit !== undefined) parts.push(session.lastExit === 0 ? 'last command succeeded' : `last command failed (exit ${session.lastExit})`); + if (session.queued) parts.push(queueStatus(session)); return parts; } const parts = [`running ${session.running.replace(/\s+/gu, ' ').slice(0, 60)} · ${formatAge(now - (session.runningSince ?? now))}`]; @@ -57,5 +58,11 @@ export function liveStatusParts(session: SessionInfo, now: number): string[] { } if (session.fullscreen) parts.push('fullscreen'); if (session.title) parts.push(`“${session.title.slice(0, 40)}”`); + if (session.queued) parts.push(queueStatus(session)); return parts; } + +/** The session's queue as the service reports it: how many wait, and whether they are paused. */ +function queueStatus(session: SessionInfo): string { + return `${session.queued} queued${session.queuePaused ? ' (paused)' : ''}`; +} diff --git a/src/setup/SetupCat.ts b/src/setup/SetupCat.ts index 590f95a3..7ee8f792 100644 --- a/src/setup/SetupCat.ts +++ b/src/setup/SetupCat.ts @@ -351,6 +351,7 @@ export const SETUP_SECTIONS: readonly SetupSection[] = [ 'Session notices are short events about another session (a finished command, a failure, a request for attention); /sessions keeps the state.'], rows: [ {row: configRow('liveSessionStartup')}, {row: configRow('liveSessionMultiple')}, + {row: configRow('commandQueue'), note: draft => draft.commandQueue ? 'On: while a command runs, Enter queues the next one; a program waiting for input still gets your answer' : 'Off: Enter while a command runs goes to the program, as in a plain terminal'}, {row: configRow('sessionNotices'), note: draft => draft.sessionNotices ? 'Brief: a success fades in seconds, a failure lingers a little, a request for attention stays until you look' : 'Off: other sessions never add lines above the composer'}, {row: configRow('terminalTitle'), note: draft => draft.terminalTitle === 'off' ? 'Off: the terminal keeps its own window title' : 'NMSh titles the window while it owns the screen; programs keep their own title while they run'}, diff --git a/src/shell/submission.ts b/src/shell/submission.ts new file mode 100644 index 00000000..85e5e8a4 --- /dev/null +++ b/src/shell/submission.ts @@ -0,0 +1,8 @@ +/** + * The text NMSh submits to the shell for a command typed (or queued) in the composer. A multi-line command is + * submitted as one brace group, so the shell reads it whole and runs it as one command: lines never start one by one, + * and a heredoc or an `if` block keeps its meaning. One rule for the composer and the queue. + */ +export function shellSubmission(command: string): string { + return command.includes('\n') ? `{ ${command}\n}` : command; +} diff --git a/src/status/queueStatus.ts b/src/status/queueStatus.ts new file mode 100644 index 00000000..0d6b3093 --- /dev/null +++ b/src/status/queueStatus.ts @@ -0,0 +1,68 @@ +import {displayWidth, truncateText} from '../util/text.js'; +import {foreground, UI_COLORS} from '../ui/palette.js'; +import {getCurrentGlyphMode} from '../ui/glyphs.js'; +import type {QueuePause, QueueState} from '../session/CommandQueue.js'; + +/** + * How NMSh shows the command queue. The queue's owner (the session service, or the in-process client) is the one + * source; this module only words and paints it. No animation: a queue waits, it does not work. + */ +const RESET = '\u001b[0m'; + +export function queueGlyph(): string { return getCurrentGlyphMode() === 'safe' ? '=' : '≡'; } +export function pausedGlyph(): string { return getCurrentGlyphMode() === 'safe' ? '||' : '⏸'; } + +/** One line for a queued command: its first line, and how many more it has. */ +export function queuePreview(text: string, limit = 80): string { + const lines = text.split('\n'); + const more = lines.length > 1 ? ` (+${lines.length - 1} line${lines.length === 2 ? '' : 's'})` : ''; + const room = Math.max(1, limit - displayWidth(more)); + return `${truncateText(lines[0]!.trim() || '(blank first line)', room)}${more}`; +} + +/** Why the queue waits, in words. */ +export function pauseReason(pause: QueuePause): string { + switch (pause.reason) { + case 'failed': return `${commandName(pause.command)} failed (exit ${pause.exitCode})`; + case 'interrupted': return `${commandName(pause.command)} was interrupted`; + case 'shell-switched': return 'the shell changed'; + case 'restored': return 'restored, not resumed'; + case 'user': return 'paused'; + } +} + +function commandName(command: string): string { + return truncateText(command.split('\n')[0]!.trim() || 'the command', 32); +} + +/** + * The queue's one line under the running command (or at idle while paused), widest form first: + * ≡ Queued (2) · next: adb devices · /queue + * ⏸ Queue paused · make failed (exit 2) · 2 waiting · /queue + * Narrow widths drop the hint, then the reason or preview; the count always stays. + */ +export function queueRow(state: QueueState, columns: number): string | undefined { + const count = state.entries.length; + if (!count || columns <= 0) return undefined; + const subtle = foreground(UI_COLORS.subtle); + const secondary = foreground(UI_COLORS.secondary); + const accent = foreground(UI_COLORS.accent); + if (state.paused) { + const head = `${pausedGlyph()} Queue paused`; + const parts = [pauseReason(state.paused), `${count} waiting`, '/queue']; + const forms = [parts, parts.slice(0, 2), [parts[1]!]]; + for (const form of forms) { + const plain = [head, ...form].join(' · '); + if (displayWidth(plain) <= columns) return `${accent}${head}${RESET}${secondary} · ${form.join(' · ')}${RESET}`; + } + return `${accent}${truncateText(`${head} · ${count}`, columns)}${RESET}`; + } + const next = state.entries[0]!; + const head = `${queueGlyph()} Queued (${count})`; + const editing = state.editing === next.id ? ' (editing)' : ''; + for (const hint of [' · /queue', '']) { + const room = columns - displayWidth(`${head} · next: `) - displayWidth(editing) - displayWidth(hint); + if (room >= 8) return `${secondary}${head}${RESET}${subtle} · next: ${queuePreview(next.text, room)}${editing}${hint}${RESET}`; + } + return `${secondary}${truncateText(head, columns)}${RESET}`; +} diff --git a/src/terminal/keys.ts b/src/terminal/keys.ts index f6485946..7b9ad9b2 100644 --- a/src/terminal/keys.ts +++ b/src/terminal/keys.ts @@ -6,7 +6,9 @@ export type Key = | {kind: 'left' | 'right' | 'up' | 'down' | 'lineHome' | 'lineEnd' | 'backspace' | 'delete' | 'enter' | 'newline' | 'complete' | 'escape' | 'selectAll'} | {kind: 'selectLeft' | 'selectRight' | 'selectUp' | 'selectDown' | 'selectLineHome' | 'selectLineEnd'} | {kind: 'bufferHome' | 'bufferEnd' | 'selectBufferHome' | 'selectBufferEnd'} - | {kind: 'historySearch' | 'historyDelete' | 'find'} | {kind: 'suggestNext' | 'suggestPrevious' | 'palette'} | {kind: 'pageUp' | 'pageDown' | 'latest' | 'interrupt' | 'suspend' | 'eof' | 'wheelUp' | 'wheelDown' | 'mouseMove' | 'mouseClick' | 'mouseDrag' | 'mouseRelease' | 'focusPrevious' | 'focusNext' | 'toggleDetails'} & {x?: number; y?: number}; + | {kind: 'historySearch' | 'historyDelete' | 'find'} | {kind: 'suggestNext' | 'suggestPrevious' | 'palette'} | {kind: 'pageUp' | 'pageDown' | 'latest' | 'interrupt' | 'suspend' | 'eof' | 'wheelUp' | 'wheelDown' | 'mouseMove' | 'mouseClick' | 'mouseDrag' | 'mouseRelease' | 'focusPrevious' | 'focusNext' | 'toggleDetails' + /** Ctrl+Q: add the composer's command to the queue; Ctrl+S: send it to the running program's input. */ + | 'queueCommand' | 'sendToProgram'} & {x?: number; y?: number}; const SEQUENCES: Array<[string, Key['kind']]> = [ @@ -99,6 +101,8 @@ const SEQUENCES: Array<[string, Key['kind']]> = [ ['\u001B[P', 'palette'], ['\u001B[110;5u', 'suggestNext'], // Kitty Ctrl+N ['\u001B[112;5u', 'suggestPrevious'], // Kitty Ctrl+P + ['\u001B[113;5u', 'queueCommand'], // Kitty Ctrl+Q + ['\u001B[115;5u', 'sendToProgram'], // Kitty Ctrl+S ['\u001B[111;5u', 'toggleDetails'], // Kitty Ctrl+O (lowercase o) ['\u001B[79;5u', 'toggleDetails'], // Kitty Ctrl+O (uppercase O) ]; @@ -171,6 +175,8 @@ export function decodeKeys(input: string): Key[] { else if (value === '\u0012') keys.push({kind: 'historySearch'} as Key); else if (value === '\u0006') keys.push({kind: 'find'} as Key); // Ctrl+F: NMSh transcript find else if (value === '\u000F') keys.push({kind: 'toggleDetails'} as Key); // Ctrl+O + else if (value === '\u0011') keys.push({kind: 'queueCommand'} as Key); // Ctrl+Q: queue (the TTY is raw; no flow control) + else if (value === '\u0013') keys.push({kind: 'sendToProgram'} as Key); // Ctrl+S: send to the running program else if (value === '\u000E') keys.push({kind: 'suggestNext'} as Key); // Ctrl+N else if (value === '\u0010') keys.push({kind: 'suggestPrevious'} as Key); // Ctrl+P else if (codePoint >= 0x20 && codePoint !== 0x7f) keys.push({kind: 'text', value} as Key); diff --git a/src/ui/SettingsPanel.ts b/src/ui/SettingsPanel.ts index a655ae03..e986be43 100644 --- a/src/ui/SettingsPanel.ts +++ b/src/ui/SettingsPanel.ts @@ -396,6 +396,8 @@ export const SETTINGS_ROWS: readonly SettingsRow[] = [ enumRow({id: 'terminalTitle', label: 'Terminal title', description: 'Set the window/tab title while NMSh owns the screen; programs set their own while they run and NMSh restores its title after them. Off leaves the title to the terminal', category: 'Sessions', values: ['off', 'project', 'session'] as const, labels: ['Off', 'Project', 'Project and session'], get: config => config.terminalTitle, set: (config, terminalTitle) => ({...config, terminalTitle})}), + {id: 'commandQueue', label: 'Queue commands', description: 'While a command runs, Enter queues the next one to run after it (Ctrl+Q always queues, Ctrl+S sends to the program); a program waiting for input still gets your answer. Off: Enter sends to the program', category: 'Sessions', + control: 'boolean', get: config => config.commandQueue, set: (config, commandQueue) => ({...config, commandQueue})}, {id: 'agentActivity', label: 'Agent activity', description: 'Local-only durations and counts for Claude Code and Codex CLI runs (/agents); never prompts or output', category: 'Sessions', control: 'boolean', get: config => config.agentActivity, set: (config, agentActivity) => ({...config, agentActivity})}, enumRow({id: 'shellBackend', label: 'Default shell', description: 'Shell for new sessions; /shell switches the current session and lists what is installed', category: 'Sessions', diff --git a/tests/commandQueue.test.ts b/tests/commandQueue.test.ts new file mode 100644 index 00000000..65f07edc --- /dev/null +++ b/tests/commandQueue.test.ts @@ -0,0 +1,150 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {CommandQueue, QUEUE_LIMIT} from '../src/session/CommandQueue.js'; +import {QueueDispatcher, type QueueEvent, type QueueShell} from '../src/session/QueueDispatcher.js'; +import type {QueueState} from '../src/session/CommandQueue.js'; + +function shell(): QueueShell & {submitted: string[]} { + return {submitted: [], isReady: true, submit(command: string) { this.submitted.push(command); }}; +} + +function harness() { + const target = shell(); + const dispatcher = new QueueDispatcher(target); + const events: Array<[QueueState, QueueEvent]> = []; + dispatcher.on('state', (state, event) => events.push([state, event])); + /** The shell runs what was submitted and finishes with `exitCode`. */ + const finish = (exitCode = 0) => { dispatcher.onExec(target.submitted.at(-1) ?? 'manual'); dispatcher.onPrompt(exitCode); }; + return {target, dispatcher, events, finish}; +} + +test('entries run one at a time, in order, each only after the previous command reached its prompt', () => { + const {target, dispatcher, events, finish} = harness(); + dispatcher.onExec('shasum -a 256 big.iso'); + dispatcher.request({op: 'add', text: 'adb devices'}); + dispatcher.request({op: 'add', text: 'adb shell pm list packages'}); + assert.deepEqual(target.submitted, [], 'nothing runs while a command runs'); + dispatcher.onPrompt(0); + assert.deepEqual(target.submitted, ['adb devices']); + assert.equal(dispatcher.state.entries.length, 1); + assert.equal(events.filter(([, event]) => event.dispatched).length, 1, 'announced once'); + dispatcher.request({op: 'add', text: 'echo third'}); + assert.deepEqual(target.submitted, ['adb devices'], 'a new entry waits for the running entry'); + finish(); + finish(); + assert.deepEqual(target.submitted, ['adb devices', 'adb shell pm list packages', 'echo third']); + finish(); + assert.equal(dispatcher.state.entries.length, 0); +}); + +test('an idle shell runs a new entry at once; a prompt without a command (empty Enter) counts as idle, not as a finish', () => { + const {target, dispatcher} = harness(); + dispatcher.onPrompt(0); + dispatcher.request({op: 'add', text: 'ls'}); + assert.deepEqual(target.submitted, ['ls']); + dispatcher.onPrompt(1); + dispatcher.request({op: 'add', text: 'pwd'}); + assert.deepEqual(target.submitted, ['ls', 'pwd'], 'an empty Enter\'s status is not a failure'); +}); + +test('a failure or an interrupt pauses the rest with the reason; nothing follows blindly; resume continues', () => { + const {target, dispatcher, finish} = harness(); + dispatcher.onExec('make build'); + dispatcher.request({op: 'add', text: 'make deploy'}); + dispatcher.onPrompt(2); + assert.deepEqual(target.submitted, []); + assert.deepEqual(dispatcher.state.paused && {...dispatcher.state.paused, at: 0}, {reason: 'failed', at: 0, command: 'make build', exitCode: 2}); + dispatcher.request({op: 'resume'}); + assert.deepEqual(target.submitted, ['make deploy']); + + dispatcher.onExec('make deploy'); + dispatcher.request({op: 'add', text: 'notify done'}); + dispatcher.onInterrupt(); + dispatcher.onPrompt(0); + assert.equal(dispatcher.state.paused?.reason, 'interrupted', 'Ctrl+C pauses even when the program exits 0'); + dispatcher.onExec('sleep 9'); + dispatcher.onPrompt(130); + assert.deepEqual(target.submitted, ['make deploy'], 'still paused'); + void finish; +}); + +test('an entry is taken exactly once: repeated prompts, edits and removals never run it twice or out of order', () => { + const {target, dispatcher} = harness(); + dispatcher.onExec('long'); + dispatcher.request({op: 'add', text: 'first'}); + dispatcher.request({op: 'add', text: 'second'}); + const [first, second] = dispatcher.state.entries; + dispatcher.request({op: 'edit-begin', id: first!.id}); + dispatcher.onPrompt(0); + assert.deepEqual(target.submitted, [], 'an entry being edited holds the queue at that point'); + dispatcher.request({op: 'edit', id: first!.id, text: 'first --edited'}); + assert.deepEqual(target.submitted, ['first --edited']); + dispatcher.onPrompt(0); + dispatcher.onPrompt(0); + assert.deepEqual(target.submitted, ['first --edited', 'second'], 'a prompt without a command does not dispatch twice'); + dispatcher.request({op: 'remove', id: second!.id}); + assert.match(String(dispatcher.queue.apply({op: 'remove', id: second!.id})), /already ran or was removed/u); +}); + +test('reorder, clear, pause and limits', () => { + const queue = new CommandQueue(() => 5); + for (const text of ['a', 'b', 'c']) queue.apply({op: 'add', text}); + const [a, , c] = queue.state.entries; + queue.apply({op: 'move', id: c!.id, to: 0}); + assert.deepEqual(queue.state.entries.map(entry => entry.text), ['c', 'a', 'b']); + queue.apply({op: 'move', id: a!.id, to: 99}); + assert.deepEqual(queue.state.entries.map(entry => entry.text), ['c', 'b', 'a']); + queue.apply({op: 'pause'}); + assert.equal(queue.take(), undefined); + queue.apply({op: 'clear'}); + assert.deepEqual(queue.state, {entries: []}); + assert.match(String(queue.apply({op: 'add', text: ' '})), /Nothing to queue/u); + for (let index = 0; index < QUEUE_LIMIT; index += 1) queue.apply({op: 'add', text: `cmd ${index}`}); + assert.match(String(queue.apply({op: 'add', text: 'one more'})), /at most/u); +}); + +test('multi-line entries are submitted whole, exactly as prepared', () => { + const {target, dispatcher} = harness(); + dispatcher.onPrompt(0); + const script = "for f in a b; do\n echo \"$f\"\ndone\ncat <<'EOF'\nliteral $HOME\nEOF"; + dispatcher.request({op: 'add', text: script}); + assert.deepEqual(target.submitted, [`{ ${script}\n}`], 'one block, exactly as the composer submits it'); +}); + +test('a shell switch pauses the queue for the new shell; a shell exit drops and reports what is left', () => { + const {target, dispatcher, events} = harness(); + dispatcher.onExec('long'); + dispatcher.request({op: 'add', text: 'echo for zsh'}); + const next = shell(); + dispatcher.onShellSwitched(next); + dispatcher.onPrompt(0); + assert.deepEqual([...target.submitted, ...next.submitted], []); + assert.equal(dispatcher.state.paused?.reason, 'shell-switched'); + dispatcher.onShellExit(); + assert.deepEqual(events.at(-1)![1].dropped?.map(entry => entry.text), ['echo for zsh']); + assert.deepEqual(dispatcher.state, {entries: []}); +}); + +test('nothing is dispatched before the shell is ready', () => { + const target = {...shell(), isReady: false}; + const dispatcher = new QueueDispatcher(target); + dispatcher.request({op: 'add', text: 'echo early'}); + dispatcher.onPrompt(0); + assert.deepEqual(target.submitted, []); +}); + +test('the exec after a dispatch is that entry, by its own text; nothing else is ever attributed to the queue', () => { + const {target, dispatcher} = harness(); + assert.equal(dispatcher.onExec('sleep 3'), undefined, 'a typed command is not from the queue'); + dispatcher.request({op: 'add', text: 'for i in 1 2; do\n echo "$i"\ndone'}); + dispatcher.onPrompt(0); + assert.equal(target.submitted.length, 1); + assert.deepEqual(dispatcher.onExec(target.submitted[0]!)?.text, 'for i in 1 2; do\n echo "$i"\ndone', 'the entry, not the wrapped block'); + dispatcher.onPrompt(0); + assert.equal(dispatcher.onExec('ls'), undefined, 'claimed once'); + // A submission that started no command (a comment) is not owed to whatever runs next. + dispatcher.request({op: 'add', text: '# just a note'}); + dispatcher.onPrompt(0); + dispatcher.onPrompt(0); + assert.equal(dispatcher.onExec('ls'), undefined); +}); diff --git a/tests/commandQueueLive.test.ts b/tests/commandQueueLive.test.ts new file mode 100644 index 00000000..7097ac70 --- /dev/null +++ b/tests/commandQueueLive.test.ts @@ -0,0 +1,256 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {spawnSync} from 'node:child_process'; +import {existsSync} from 'node:fs'; +import {join, resolve} from 'node:path'; +import {LiveSandbox, strip, until, type Frontend} from './helpers/liveFrontend.js'; + +/** + * The command queue through the real composer, PTY and session service, with harmless commands only (sleep, echo, + * cd, touch inside the sandbox). Entries run in the session's own shell, in order, after the shell's own prompt. + */ +const ASKYN = resolve('tests/fixtures/askyn.py'); +const bash = ['/opt/homebrew/bin/bash', '/usr/local/bin/bash', '/usr/bin/bash', '/bin/bash'] + .find(path => existsSync(path) && /version (?:4\.[4-9]|[5-9]\.)/u.test(spawnSync(path, ['--version'], {encoding: 'utf8'}).stdout ?? '')); +const fish = ['/opt/homebrew/bin/fish', '/usr/local/bin/fish', '/usr/bin/fish'].find(existsSync); +const python = spawnSync('python3', ['-c', 'import termios'], {encoding: 'utf8'}).status === 0; +const supported = process.platform === 'darwin' || process.platform === 'linux'; +type Shell = 'zsh' | 'bash' | 'fish'; + +async function start(shell: Shell, options: {size?: {cols: number; rows: number}; env?: Record} = {}) { + const sandbox = new LiveSandbox({provider: 'none', shellBackend: shell}, + shell === 'bash' && bash ? {PATH: `${bash.replace(/\/bash$/u, '')}:${process.env.PATH}`} : {}); + const frontend = sandbox.launch([], options.size ?? {cols: 100, rows: 30}, options.env ?? {}); + try { + await frontend.waitFor(/notMyShell|zsh|bash|fish/u); + await frontend.run('echo READY', /READY/u); + } catch (error) { + // A start that never became ready is not inside the test's try/finally: end its processes here, or they keep + // the test file (and the suite) running. + await sandbox.dispose().catch(() => {}); + throw error; + } + return {sandbox, frontend}; +} + +const drawn = (frontend: Frontend, from: number) => strip(frontend.output.slice(from)); +const pause = (ms: number) => new Promise(done => setTimeout(done, ms)); + +const SET_STATE: Record = { + zsh: ['cd sub', 'export QUEUE_VAR=kept', 'qfn() { echo "fn-$1" }', 'setopt nullglob'], + bash: ['cd sub', 'export QUEUE_VAR=kept', 'qfn() { echo "fn-$1"; }', 'shopt -s nullglob'], + fish: ['cd sub', 'set -gx QUEUE_VAR kept', 'function qfn; echo "fn-$argv[1]"; end', 'set -g fish_queue_opt on'], +}; +const CHECK_STATE: Record = { + zsh: 'echo "STATE $(basename $PWD) $QUEUE_VAR $(qfn x) $([[ -o nullglob ]] && echo opt)"', + bash: 'echo "STATE $(basename $PWD) $QUEUE_VAR $(qfn x) $(shopt -q nullglob && echo opt)"', + fish: 'echo "STATE "(basename $PWD)" $QUEUE_VAR "(qfn x)" $fish_queue_opt"', +}; + +for (const shell of ['zsh', 'bash', 'fish'] as const) { + const available = supported && (shell === 'zsh' || (shell === 'bash' ? bash : fish)); + test(`live ${shell}: Enter while a command runs queues; entries run in order in the same shell, keeping its state`, {skip: available ? false : `${shell} not installed`, timeout: 120_000}, async () => { + const {sandbox, frontend} = await start(shell); + try { + await frontend.run('mkdir -p sub', /❯/u); + const mark = frontend.mark; + frontend.pty.write('sleep 3\r'); + await frontend.waitFor(/Running sleep 3/u, mark); + for (const line of [...SET_STATE[shell], CHECK_STATE[shell]]) { + frontend.pty.write(`${line}\r`); + await pause(80); + } + await frontend.waitFor(/≡ Queued \(5\) · next: cd sub/u, mark); + assert.match(drawn(frontend, mark), /Enter queues the next command · Ctrl\+S sends to sleep/u, 'the empty composer says what Enter does'); + await frontend.waitFor(/STATE sub kept fn-x (?:opt|on)/u, mark, 30_000); + const commands = ['sleep 3', ...SET_STATE[shell], CHECK_STATE[shell]]; + const records = async () => (await sandbox.transcripts().list()).flatMap(session => session.transcript.records).filter(record => commands.includes(record.command)); + await until(async () => (await records()).length >= commands.length, 20_000, 'every command recorded'); + const recorded = (await records()).reverse(); + assert.deepEqual(recorded.map(record => record.command), commands, 'in order, each once'); + assert.deepEqual(recorded.map(record => /from queue/u.test(record.lifecycleText)), [false, true, true, true, true, true], + `queued commands say so: ${recorded.map(record => record.lifecycleText).join(' | ')}`); + assert.doesNotMatch(drawn(frontend, mark), /command not found: (?:cd|export|qfn)/u, 'nothing reached sleep as input or ran early'); + } finally { await sandbox.dispose(); } + }); +} + +test('live: a failed command pauses the queue with its reason; nothing after it runs until resumed or cleared', {skip: supported ? false : 'unsupported platform', timeout: 120_000}, async () => { + const {sandbox, frontend} = await start('zsh'); + try { + const marker = join(sandbox.home, 'deployed'); + const mark = frontend.mark; + frontend.pty.write(`sh -c 'sleep 1; exit 3'\r`); + await frontend.waitFor(/Running sh -c/u, mark); + frontend.pty.write(`touch ${marker}\r`); + await frontend.waitFor(/Queue paused: sh -c 'sleep 1; exit 3' failed \(exit 3\)\. 1 waiting · \/queue to resume or clear/u, mark, 20_000); + await frontend.waitFor(/⏸ Queue paused · sh -c 'sleep 1; exit 3' failed \(exit 3\) · 1 waiting/u, mark); + await pause(1500); + assert.equal(existsSync(marker), false, 'the follow-up did not run blindly'); + // A command typed at the prompt still runs at once; the paused queue keeps waiting. + await frontend.run('echo DIRECT', /DIRECT/u); + assert.equal(existsSync(marker), false); + const resume = frontend.mark; + frontend.pty.write('/queue resume\r'); + await frontend.waitFor(/Queue resumed; 1 will run in order/u, resume); + await until(() => existsSync(marker), 15_000, 'the resumed entry runs'); + await frontend.waitFor(/from queue/u, resume); + } finally { await sandbox.dispose(); } +}); + +test('live: a program waiting for a line gets Enter; Ctrl+Q queues; Ctrl+S sends to a program NMSh cannot see waiting', {skip: supported ? false : 'unsupported platform', timeout: 120_000}, async () => { + const {sandbox, frontend} = await start('zsh'); + try { + let mark = frontend.mark; + frontend.pty.write(`read 'x?Name: '; echo got=$x\r`); + await frontend.waitFor(/[◆◇] (?:Probably w|W)aiting for input[\s\S]*Name:/u, mark); + await frontend.waitFor(/Enter answers zsh · Ctrl\+Q queues a command/u, mark); + frontend.pty.write('echo QUEUED-AFTER-READ\u0011'); + await frontend.waitFor(/≡ 1 queued|≡ Queued \(1\)/u, mark); + frontend.pty.write('alice\r'); + await frontend.waitFor(/got=alice[\s\S]*QUEUED-AFTER-READ/u, mark, 20_000); + assert.doesNotMatch(drawn(frontend, mark), /got=echo/u, 'the queued command never reached read'); + + // cat reads stdin with no prompt: NMSh cannot tell it waits, so Enter queues; Ctrl+S sends the line to cat. + mark = frontend.mark; + frontend.pty.write('cat\r'); + await frontend.waitFor(/Running cat/u, mark); + frontend.pty.write('to-cat'); + await frontend.waitFor(/❯ to-cat/u, mark); + // After Ctrl+S: the terminal's echo of the line, then cat's own copy of it (composer redraws may sit between). + const sent = frontend.mark; + frontend.pty.write('\u0013'); + await frontend.waitFor(/to-cat[\s\S]*to-cat/u, sent); + frontend.pty.write('\u0004'); + await frontend.waitFor(/Completed/u, mark); + } finally { await sandbox.dispose(); } +}); + +test('live: a reply typed at a fresh prompt goes to the program before any probe looks; it is never queued or run', {skip: supported ? false : 'unsupported platform', timeout: 120_000}, async () => { + const {sandbox, frontend} = await start('zsh'); + try { + const mark = frontend.mark; + // The prompt is assembled at run time, so only the program's own question matches (not the echoed command). + frontend.pty.write(`p=Pass; sleep 0.3; read -s "x?\${p}word: "; echo; echo "len=\${#x}"\r`); + await frontend.waitFor(/Password: /u, mark); + // At once: no input probe has run yet, only the question on the program's line is evidence. + frontend.pty.write('hunter2\r'); + await frontend.waitFor(/len=7[\s\S]*Completed/u, mark, 20_000); + await pause(800); + const painted = drawn(frontend, mark); + assert.doesNotMatch(painted, /≡ (?:\d+ q|Queued)/u, 'the reply was never queued'); + assert.doesNotMatch(painted, /hunter2/u, 'the hidden reply was never shown or run'); + const records = (await sandbox.transcripts().list()).flatMap(session => session.transcript.records); + assert.equal(records.some(record => record.command.includes('hunter2')), false, 'never recorded as a command'); + } finally { await sandbox.dispose(); } +}); + +test('live: during single-key input, Ctrl+Q composes a queue entry the program never sees; Esc returns keys to it', {skip: supported && python ? false : 'python3 with termios not available', timeout: 120_000}, async () => { + const {sandbox, frontend} = await start('zsh'); + try { + const mark = frontend.mark; + frontend.pty.write(`python3 ${ASKYN} 'Fix? '\r`); + await frontend.waitFor(/◆ Waiting for input[\s\S]*Ctrl\+Q queues a command/u, mark); + frontend.pty.write('\u0011'); + await frontend.waitFor(/Queue a command · Enter adds it · Esc returns to the program/u, mark); + frontend.pty.write('echo AFTER-ASKYN\r'); + await frontend.waitFor(/≡ 1 queued/u, mark); + frontend.pty.write('y'); + await frontend.waitFor(/ANSWERS=yes[\s\S]*AFTER-ASKYN/u, mark, 20_000); + assert.doesNotMatch(drawn(frontend, mark), /ANSWERS=[^\n]*,/u, 'exactly one key reached the program'); + } finally { await sandbox.dispose(); } +}); + +test('live: Ctrl+C pauses what was queued behind the interrupted command', {skip: supported ? false : 'unsupported platform', timeout: 120_000}, async () => { + const {sandbox, frontend} = await start('zsh'); + try { + const marker = join(sandbox.home, 'after-interrupt'); + const mark = frontend.mark; + frontend.pty.write('sleep 30\r'); + await frontend.waitFor(/Running sleep 30/u, mark); + frontend.pty.write(`touch ${marker}\r`); + await frontend.waitFor(/≡ Queued \(1\)/u, mark); + frontend.pty.write('\u0003'); + await frontend.waitFor(/Queue paused: sleep 30 was interrupted/u, mark, 20_000); + await pause(1000); + assert.equal(existsSync(marker), false); + const clear = frontend.mark; + frontend.pty.write('/queue clear\r'); + await frontend.waitFor(/Cleared 1 queued command; none of them ran\./u, clear); + } finally { await sandbox.dispose(); } +}); + +test('live: after Ctrl+Z or Ctrl+C, what you type next is for the shell prompt, not a queue entry', {skip: supported ? false : 'unsupported platform', timeout: 120_000}, async () => { + const {sandbox, frontend} = await start('zsh'); + try { + const mark = frontend.mark; + frontend.pty.write('sleep 177\r'); + await frontend.waitFor(/Running sleep 177/u, mark); + // Typed at once, before NMSh sees the prompt the stop brings back (as the GNU screen test does). + frontend.pty.write('\u001a'); + frontend.pty.write('jobs\r'); + await frontend.waitFor(/suspended\s+sleep 177[\s\S]*suspended\s+sleep 177/u, mark, 20_000); + await frontend.run('kill %1; echo JOB-GONE', /JOB-GONE/u); + frontend.pty.write('sleep 178\r'); + await frontend.waitFor(/Running sleep 178/u, mark); + frontend.pty.write('\u0003'); + frontend.pty.write('echo AFTER-STOP\r'); + await frontend.waitFor(/AFTER-STOP[\s\S]*Completed/u, mark, 20_000); + assert.doesNotMatch(drawn(frontend, mark), /Queue paused/u, 'nothing typed after a stop was held as a queue entry'); + } finally { await sandbox.dispose(); } +}); + +test('live: a multi-line paste queues as one entry and runs once, as one block', {skip: supported ? false : 'unsupported platform', timeout: 120_000}, async () => { + const {sandbox, frontend} = await start('zsh'); + try { + const mark = frontend.mark; + frontend.pty.write('sleep 5\r'); + await frontend.waitFor(/Running sleep 5/u, mark); + frontend.pty.write('\u001b[200~for i in 1 2; do\n echo "LINE-$i"\ndone\u001b[201~'); + // NMSh's paste review comes first; Enter inserts the paste into the composer, and only the next Enter queues it. + await frontend.waitFor(/Enter insert · R review · Esc cancel/u, mark); + frontend.pty.write('\r'); + await frontend.waitFor(/❯ for i in 1 2; do/u, mark); + await pause(300); + assert.doesNotMatch(drawn(frontend, mark), /≡ Queued/u, 'a paste never queues by itself'); + frontend.pty.write('\r'); + await frontend.waitFor(/≡ Queued \(1\) · next: for i in 1 2; do \(\+2 lines\)/u, mark); + await frontend.waitFor(/LINE-1[\s\S]*LINE-2[\s\S]*from queue/u, mark, 20_000); + assert.equal(drawn(frontend, mark).match(/LINE-1/gu)?.length, 1, 'it ran exactly once'); + } finally { await sandbox.dispose(); } +}); + +test('live: the queue belongs to the session: it survives detach and reattach and runs in its own shell', {skip: supported ? false : 'unsupported platform', timeout: 150_000}, async () => { + const {sandbox, frontend: first} = await start('zsh'); + try { + const marker = join(sandbox.home, 'ran-detached'); + const mark = first.mark; + first.pty.write('sleep 4\r'); + await first.waitFor(/Running sleep 4/u, mark); + first.pty.write(`touch ${marker}\r`); + first.pty.write('sleep 6\r'); + await first.waitFor(/≡ Queued \(2\)/u, mark); + const [session] = await sandbox.sessions(); + first.pty.kill('SIGKILL'); + await until(async () => (await sandbox.sessions()).find(item => item.id === session!.id)?.state === 'detached', 15_000, 'detached'); + // The queue keeps running while no window is attached. + await until(() => existsSync(marker), 20_000, 'the first entry ran while detached'); + const back = sandbox.launch(['--attach', session!.id]); + await back.waitFor(/Running sleep 6|Completed[\s\S]*from queue/u, 0, 30_000); + await back.waitFor(/sleep 6[\s\S]*from queue/u, 0, 30_000); + } finally { await sandbox.dispose(); } +}); + +test('live: narrow, NO_COLOR and Safe glyphs keep the queue legible', {skip: supported ? false : 'unsupported platform', timeout: 120_000}, async () => { + const {sandbox, frontend} = await start('zsh', {size: {cols: 40, rows: 18}, env: {NO_COLOR: '1', NMSH_ICONS: 'safe'}}); + try { + const mark = frontend.mark; + frontend.pty.write('sleep 3\r'); + await frontend.waitFor(/Running/u, mark); + frontend.pty.write('echo NARROW-QUEUED\r'); + // 40 columns: the entry's text is cut to fit, never wrapped into the composer. + await frontend.waitFor(/= Queued \(1\) · next: echo NARR/u, mark); + assert.doesNotMatch(drawn(frontend, mark), /≡|⏸/u, 'Safe glyphs only'); + await frontend.waitFor(/NARROW-QUEUED[\s\S]*from queue/u, mark, 20_000); + } finally { await sandbox.dispose(); } +}); diff --git a/tests/inputAwarenessLive.test.ts b/tests/inputAwarenessLive.test.ts index cdda7d62..9d528ada 100644 --- a/tests/inputAwarenessLive.test.ts +++ b/tests/inputAwarenessLive.test.ts @@ -30,9 +30,15 @@ async function start(shell: Shell, options: {size?: {cols: number; rows: number} shell === 'bash' && bash ? {PATH: `${bash.replace(/\/bash$/u, '')}:${process.env.PATH}`} : {}); const started = Date.now(); const frontend = sandbox.launch(options.args ?? [], options.size ?? {cols: 100, rows: 30}, options.env ?? {}); - await frontend.waitFor(/Vespyr|notMyShell|zsh|bash|fish/u); - await until(() => existsSync(join(sandbox.config, 'nmsh', 'theme-bridge', `environment.${shell}`)) || frontend.output.length > 0 && Date.now() - started > 8000, 20_000, 'the managed shell'); - await frontend.run('echo READY', /READY/u); + try { + await frontend.waitFor(/Vespyr|notMyShell|zsh|bash|fish/u); + await until(() => existsSync(join(sandbox.config, 'nmsh', 'theme-bridge', `environment.${shell}`)) || frontend.output.length > 0 && Date.now() - started > 8000, 20_000, 'the managed shell'); + await frontend.run('echo READY', /READY/u); + } catch (error) { + // Not inside the test's try/finally yet: a sandbox given by the caller is the caller's to end. + if (!options.sandbox) await sandbox.dispose().catch(() => {}); + throw error; + } return {sandbox, frontend}; } diff --git a/tests/sessionProtocol.test.ts b/tests/sessionProtocol.test.ts index e144ae69..dd60b972 100644 --- a/tests/sessionProtocol.test.ts +++ b/tests/sessionProtocol.test.ts @@ -20,6 +20,8 @@ const samples: ProtocolMessage[] = [ journalId: 'j1', ackedSeq: 17}, {type: 'ack', seq: 17, journalId: 'j1'}, {type: 'exec', command: 'make', seq: 18, at: 1700000000001}, + {type: 'exec', command: '{ for i in 1 2; do\n echo $i\ndone\n}', seq: 18, at: 1700000000001, queued: 'for i in 1 2; do\n echo $i\ndone'}, + {type: 'attached', sessionId: 's1', pid: 42, cwd: '/tmp', fullscreen: 0, running: 'sleep 6', runningSince: 1, runningQueued: 'sleep 6', ackedSeq: 3}, {type: 'output', data: 'x', seq: 19, at: 1700000000002}, {type: 'prompt', exitCode: 2, cwd: '/w', seq: 20, at: 1700000000003}, {type: 'replayed', truncatedBytes: 0}, From 15147d1ffc791ce16a96ac4f23adf6a675128d7b Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:20:40 +0530 Subject: [PATCH 023/249] Queue: command text is drawn, never interpreted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entry previews, pause reasons and the panel's block preview show control characters visibly (␛, ·) instead of writing them to the terminal: a pasted OSC 52 or screen clear inside a queued command can no longer act on the host terminal. --- src/queue/QueuePanel.ts | 3 ++- src/status/queueStatus.ts | 6 ++++-- tests/commandQueue.test.ts | 12 ++++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/queue/QueuePanel.ts b/src/queue/QueuePanel.ts index f838a44d..672ab265 100644 --- a/src/queue/QueuePanel.ts +++ b/src/queue/QueuePanel.ts @@ -5,6 +5,7 @@ import {focusForeground, foreground, UI_COLORS} from '../ui/palette.js'; import {GLYPHS} from '../ui/glyphs.js'; import {displayWidth, truncateAnsi, truncateText} from '../util/text.js'; import {pauseReason, pausedGlyph, queueGlyph, queuePreview} from '../status/queueStatus.js'; +import {displaySafe} from '../input/PasteReview.js'; /** * `/queue`: the session's command queue as a keyboard-first list. It holds no queue of its own: every change is a @@ -79,7 +80,7 @@ export function renderQueuePanel(panel: QueuePanelState, queue: QueueState, runn const selected = entries[panel.selected]; if (selected && selected.text.includes('\n') && height >= out.length + 8) { const lines = selected.text.split('\n'); - out.push('', ` ${subtle}Runs as one block:${reset}`, ...lines.slice(0, 5).map(line => ` ${secondary}${truncateText(line, width)}${reset}`)); + out.push('', ` ${subtle}Runs as one block:${reset}`, ...lines.slice(0, 5).map(line => ` ${secondary}${truncateText(displaySafe(line), width)}${reset}`)); if (lines.length > 5) out.push(` ${subtle}… ${lines.length - 5} more line${lines.length === 6 ? '' : 's'}${reset}`); } out.push(''); diff --git a/src/status/queueStatus.ts b/src/status/queueStatus.ts index 0d6b3093..b75851f9 100644 --- a/src/status/queueStatus.ts +++ b/src/status/queueStatus.ts @@ -1,4 +1,5 @@ import {displayWidth, truncateText} from '../util/text.js'; +import {displaySafe} from '../input/PasteReview.js'; import {foreground, UI_COLORS} from '../ui/palette.js'; import {getCurrentGlyphMode} from '../ui/glyphs.js'; import type {QueuePause, QueueState} from '../session/CommandQueue.js'; @@ -17,7 +18,8 @@ export function queuePreview(text: string, limit = 80): string { const lines = text.split('\n'); const more = lines.length > 1 ? ` (+${lines.length - 1} line${lines.length === 2 ? '' : 's'})` : ''; const room = Math.max(1, limit - displayWidth(more)); - return `${truncateText(lines[0]!.trim() || '(blank first line)', room)}${more}`; + // Command text is drawn, never interpreted: a pasted escape sequence shows as ␛, it does not reach the terminal. + return `${truncateText(displaySafe(lines[0]!.trim()) || '(blank first line)', room)}${more}`; } /** Why the queue waits, in words. */ @@ -32,7 +34,7 @@ export function pauseReason(pause: QueuePause): string { } function commandName(command: string): string { - return truncateText(command.split('\n')[0]!.trim() || 'the command', 32); + return truncateText(displaySafe(command.split('\n')[0]!.trim()) || 'the command', 32); } /** diff --git a/tests/commandQueue.test.ts b/tests/commandQueue.test.ts index 65f07edc..92b40554 100644 --- a/tests/commandQueue.test.ts +++ b/tests/commandQueue.test.ts @@ -148,3 +148,15 @@ test('the exec after a dispatch is that entry, by its own text; nothing else is dispatcher.onPrompt(0); assert.equal(dispatcher.onExec('ls'), undefined); }); + +test('queue text is drawn, never interpreted: control sequences in entries and pause reasons are neutralized', async () => { + const {queuePreview, pauseReason, queueRow} = await import('../src/status/queueStatus.js'); + const {renderQueuePanel} = await import('../src/queue/QueuePanel.js'); + const hostile = 'echo hi\u001b]52;c;cm0gLXJmIH4=\u0007\u001b[2J\u009b31m'; + for (const text of [queuePreview(hostile), pauseReason({reason: 'failed', at: 1, command: hostile, exitCode: 2}), + queueRow({entries: [{id: 1, text: hostile, addedAt: 1}]}, 120) ?? '', + renderQueuePanel({selected: 0}, {entries: [{id: 1, text: `${hostile}\nsecond ${hostile}`, addedAt: 1}]}, hostile, 120, 30).join('\n')]) { + assert.doesNotMatch(text.replace(/\u001b\[[\d;]*m/gu, ''), /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/u, JSON.stringify(text)); + } + assert.match(queuePreview(hostile), /␛\]52/u, 'visible, not executed'); +}); From a2574384d00d2802098ae67851bee36740698984 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:23:09 +0530 Subject: [PATCH 024/249] Queue: the type-ahead window after Ctrl+C or Ctrl+Z is a moment, not the rest of the command Typing after a stop goes to the shell prompt only for two seconds. A program that survives the signal (a REPL, a remote shell, a trap) gets the ordinary routing again, so later typing queues as usual and is never written into that program's input unasked. --- src/app/TerminalApp.ts | 26 ++++++++++++++++++-------- tests/commandQueueLive.test.ts | 16 ++++++++++++++++ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/app/TerminalApp.ts b/src/app/TerminalApp.ts index 83e601f9..c984ca69 100644 --- a/src/app/TerminalApp.ts +++ b/src/app/TerminalApp.ts @@ -347,6 +347,8 @@ const AGENT_DISCOVERY_MS = 15_000; const SHELF_IDLE_MS = 6000; const SUCCESS = lazyForeground(UI_COLORS.success); const ERROR = lazyForeground(UI_COLORS.failure); +/** How long after Ctrl+C or Ctrl+Z typing is taken as type-ahead for the shell prompt rather than queued. */ +const STOP_TYPE_AHEAD_MS = 2000; const clipboardFailure = (error: unknown): string => error instanceof ClipboardUnavailableError ? error.message : 'Clipboard copy failed'; /** Keys that edit or submit the composer; in Flow they bring a scrolled-back view back to it. */ const FLOW_EDIT_KEYS: ReadonlySet = new Set(['text', 'paste', 'backspace', 'delete', 'deleteWord', @@ -715,9 +717,11 @@ export class TerminalApp { private running?: {command: string; startedAt: number; interrupted: boolean; cleared: boolean; startId: number; cwd: string; historyAllowed?: number; awaitingExec?: boolean; /** Started from the command queue rather than typed at the prompt. */ queued?: boolean; - /** You sent Ctrl+C or Ctrl+Z: the command is ending at your request, so what you type next is for the shell's - * prompt (type-ahead, as in any terminal), not a queue entry held behind the pause that stop causes. */ - stopping?: boolean}; + /** When you sent Ctrl+C or Ctrl+Z: for a moment the command is ending at your request, so what you type next is + * for the shell's prompt (type-ahead, as in any terminal), not a queue entry held behind the pause that stop + * causes. Only for STOP_TYPE_AHEAD_MS: a program that survives the signal (a REPL, a remote shell) gets the + * ordinary routing again, so later typing is never sent into it unasked. */ + stoppedAt?: number}; private hoveredLineIndex?: number; /** NMSh-owned transcript selection (plain drag); presentation only. */ private selection?: TranscriptSelection; @@ -1945,7 +1949,7 @@ export class TerminalApp { this.clearCorrection(); if (this.running) { this.running.interrupted = true; - this.running.stopping = true; + this.running.stoppedAt = Date.now(); this.editor.clear(); this.session.interrupt(); } else { @@ -1965,7 +1969,7 @@ export class TerminalApp { // Job control belongs to zsh: forward ^Z so it stops the foreground job. // With no foreground command there is nothing to suspend, so the idle // composer ignores it rather than treating it as text, undo, or exit. - if (this.running) { this.running.stopping = true; this.session.write('\u001A'); } + if (this.running) { this.running.stoppedAt = Date.now(); this.session.write('\u001A'); } return; } if (key.kind === 'selectAll') { @@ -8979,7 +8983,7 @@ export class TerminalApp { /** While a command runs, the empty composer says what Enter will do with what you type. */ private composerHint(): string | undefined { - if (!this.running || this.passthrough || this.queueEdit || !this.queueAvailable() || this.running.stopping) return undefined; + if (!this.running || this.passthrough || this.queueEdit || !this.queueAvailable() || this.justStopped()) return undefined; // The program's question keeps the activity rows, so the composer itself says how compose mode ends. if (this.queueCompose) return 'Queue a command · Enter adds it · Esc returns to the program'; const request = this.inputState?.request; @@ -8987,6 +8991,12 @@ export class TerminalApp { return this.answersByDefault() ? `Enter answers ${program} · Ctrl+Q queues a command` : `Enter queues the next command · Ctrl+S sends to ${program}`; } + /** Ctrl+C or Ctrl+Z was sent moments ago: typing is type-ahead for the prompt the stop brings back. */ + private justStopped(): boolean { + const at = this.running?.stoppedAt; + return at !== undefined && Date.now() - at < STOP_TYPE_AHEAD_MS; + } + /** * Enter goes to the running program: it is known to wait for input, or it left a question open on its line * ("Password: ", "Continue? [y/N] ") that no probe has looked at yet. A reply typed at a fresh prompt (a password @@ -9074,7 +9084,7 @@ export class TerminalApp { void this.submit(); return; } - const toProgram = intent === 'send' || (intent === 'enter' && (!this.queueAvailable() || !text.trim() || this.answersByDefault() || this.passthrough || Boolean(this.running.stopping))); + const toProgram = intent === 'send' || (intent === 'enter' && (!this.queueAvailable() || !text.trim() || this.answersByDefault() || this.passthrough || this.justStopped())); if (toProgram) { this.editor.clear(); this.runningDraft = undefined; @@ -9155,7 +9165,7 @@ export class TerminalApp { /** Forward one key to the waiting program as the bytes a terminal sends; nothing is kept, shown or logged. */ private sendDirectKey(key: Key): void { if (key.kind === 'interrupt') { - if (this.running) { this.running.interrupted = true; this.running.stopping = true; } + if (this.running) { this.running.interrupted = true; this.running.stoppedAt = Date.now(); } this.session.interrupt(); return; } diff --git a/tests/commandQueueLive.test.ts b/tests/commandQueueLive.test.ts index 7097ac70..99f17239 100644 --- a/tests/commandQueueLive.test.ts +++ b/tests/commandQueueLive.test.ts @@ -200,6 +200,22 @@ test('live: after Ctrl+Z or Ctrl+C, what you type next is for the shell prompt, } finally { await sandbox.dispose(); } }); +test('live: a program that survives Ctrl+C gets ordinary routing again: later typing queues, it is never sent into the program', {skip: supported ? false : 'unsupported platform', timeout: 120_000}, async () => { + const {sandbox, frontend} = await start('zsh'); + try { + const mark = frontend.mark; + // Ignores SIGINT and keeps reading stdin: anything sent to it would be echoed back as GOT=... + frontend.pty.write(`sh -c 'trap "" INT; sleep 5; if read -t 1 line; then echo "GOT=$line"; fi; echo SURVIVED'\r`); + await frontend.waitFor(/Running sh -c/u, mark); + frontend.pty.write('\u0003'); + await pause(2600); + frontend.pty.write('echo LATER\r'); + await frontend.waitFor(/≡ Queued \(1\) · next: echo LATER/u, mark); + await frontend.waitFor(/SURVIVED[\s\S]*LATER/u, mark, 20_000); + assert.doesNotMatch(drawn(frontend, mark), /GOT=echo LATER/u, 'nothing typed after the window reached the program'); + } finally { await sandbox.dispose(); } +}); + test('live: a multi-line paste queues as one entry and runs once, as one block', {skip: supported ? false : 'unsupported platform', timeout: 120_000}, async () => { const {sandbox, frontend} = await start('zsh'); try { From 7a94ce52d107db69745adda284fad81ae5a327d1 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:18:32 +0530 Subject: [PATCH 025/249] Copy: select outputs by number, count, range or list; a picker; sticky Copy; Smart folds long output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /copy and its alias /cp: - /copy N, /copy -N (latest N), /copy A-B, /copy 1,3-5, /copy latest, /copy ui; outputs join oldest first, overlaps once. A missing number refuses the whole copy; nothing is half-copied. - --status / --no-status override "Include completion status" (Off by default) for one copy: each output is followed by the status NMSh recorded for it (outcome, exit code, time, waits). Stored output is never changed; a silent command copies its status alone only when status is asked for. - Settings → Copy (also /copy settings): Quick Copy or Interactive Picker for a bare /copy, Include completion status, Auto-expand copied output. Persisted and part of settings export/import. - Interactive Picker: newest first with /copy numbers, Space/a to select, / to search, preview of the highlighted output, s to toggle statuses (shown before Enter), Enter copies oldest first. - Feedback is a brief note above the composer after the clipboard accepted the text, never a transcript line; mistakes in a typed /copy still answer in the transcript. Blocks: - A direct [Copy] beside [Actions] where the row has room; it reads "✓ Copied" in place (same width, Actions never moves). Copy output and Copy command + output follow the status setting; Copy command never does. - While a completed block's header is scrolled away, its Copy and Actions ride on the sticky header row, owned by the block's startId; one set of controls on screen at a time. NMSh's viewport only: native terminal scrollback is not something NMSh can decorate. Captures: docs/captures/sticky-*. Folding and copying are independent: no copy path changes what is folded. With "Auto-expand copied output" On, only the blocks whose text was copied unfold, after a successful write. Smart Folding: long output (120+ lines, or as many rows when long lines wrap at 120 columns) now folds whatever the command or exit status. Before, varied diagnostics (adb getprop, ps, env) and any output containing a single "error" word never folded. Up to three important lines from the hidden middle (errors, compiler locations, the first frame of a trace) stay in view, and the summary counts them. A block is never collapsed at completion while you are scrolled back or selecting. Shorter output keeps the existing content heuristics. --- docs/captures/sticky-1-beginning.png | Bin 0 -> 170167 bytes docs/captures/sticky-2-middle.png | Bin 0 -> 175235 bytes docs/captures/sticky-3-end.png | Bin 0 -> 175719 bytes docs/captures/sticky-4-transition.png | Bin 0 -> 175719 bytes docs/captures/sticky-5-next-block.png | Bin 0 -> 153294 bytes docs/captures/sticky-controls.gif | Bin 0 -> 347549 bytes scripts/demos/sticky-controls.tape | 33 +++++ src/app/TerminalApp.ts | 179 +++++++++++++++++++----- src/ask/concepts.ts | 6 +- src/clipboard/copySelection.ts | 124 ++++++++++++++++ src/commands/slashCommands.ts | 16 ++- src/configuration/portability.ts | 1 + src/output/FoldPolicy.ts | 43 +++++- src/output/OutputBuffer.ts | 49 ++++++- src/output/TranscriptPresenter.ts | 26 +++- src/prompt/configuration.ts | 26 +++- src/setup/SetupCat.ts | 3 + src/ui/BlockActions.ts | 50 +++++-- src/ui/CopyPicker.ts | 145 +++++++++++++++++++ src/ui/RowPanel.ts | 9 +- src/ui/SettingsPanel.ts | 10 +- tests/autocomplete.test.ts | 2 +- tests/copyFolding.test.ts | 131 +++++++++++++++++ tests/copyPayload.test.ts | 3 +- tests/copySelection.test.ts | 194 ++++++++++++++++++++++++++ tests/copySelectionLive.test.ts | 175 +++++++++++++++++++++++ tests/historySearch.test.ts | 2 +- tests/smartFolding.test.ts | 85 ++++++++--- tests/stickyControls.test.ts | 144 +++++++++++++++++++ 29 files changed, 1363 insertions(+), 93 deletions(-) create mode 100644 docs/captures/sticky-1-beginning.png create mode 100644 docs/captures/sticky-2-middle.png create mode 100644 docs/captures/sticky-3-end.png create mode 100644 docs/captures/sticky-4-transition.png create mode 100644 docs/captures/sticky-5-next-block.png create mode 100644 docs/captures/sticky-controls.gif create mode 100644 scripts/demos/sticky-controls.tape create mode 100644 src/clipboard/copySelection.ts create mode 100644 src/ui/CopyPicker.ts create mode 100644 tests/copyFolding.test.ts create mode 100644 tests/copySelection.test.ts create mode 100644 tests/copySelectionLive.test.ts create mode 100644 tests/stickyControls.test.ts diff --git a/docs/captures/sticky-1-beginning.png b/docs/captures/sticky-1-beginning.png new file mode 100644 index 0000000000000000000000000000000000000000..0c7a12c6cc5ced27911062a4c10996b55aaab032 GIT binary patch literal 170167 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

HVWOQOiCXDaqP9;lLT81>3)#vMxcZ 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 M(=h-cuiHob2k@!d8~^|S literal 0 HcmV?d00001 diff --git a/docs/captures/sticky-controls.gif b/docs/captures/sticky-controls.gif new file mode 100644 index 0000000000000000000000000000000000000000..783321c54257cd29fa3287531a31dca96e09f9fd GIT binary patch literal 347549 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

9uQ1{uCSq$3F^8k+I;9&t-np^39}LjYumRrq`o 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

J8;61P69QQOl)z*1x(s*fx&lE|iQfkHB$(^;-$` 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

d(`tW-Kl~j4 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/captures/sticky-2-middle.png b/docs/captures/sticky-2-middle.png new file mode 100644 index 0000000000000000000000000000000000000000..ed247979ad14307a0c9428ad8c3685eeeeef4562 GIT binary patch literal 175235 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%0ASmbsNN*GxzQQQA7?Z7WIzL>|Ut2-Uc*V52 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 literal 0 HcmV?d00001 diff --git a/docs/captures/sticky-3-end.png b/docs/captures/sticky-3-end.png new file mode 100644 index 0000000000000000000000000000000000000000..7274d8dc1f472d8cdd59758a42db36a714572975 GIT binary patch literal 175719 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 M%d5&&$e0HHU-`I}?EnA( literal 0 HcmV?d00001 diff --git a/docs/captures/sticky-4-transition.png b/docs/captures/sticky-4-transition.png new file mode 100644 index 0000000000000000000000000000000000000000..7274d8dc1f472d8cdd59758a42db36a714572975 GIT binary patch literal 175719 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 M%d5&&$e0HHU-`I}?EnA( literal 0 HcmV?d00001 diff --git a/docs/captures/sticky-5-next-block.png b/docs/captures/sticky-5-next-block.png new file mode 100644 index 0000000000000000000000000000000000000000..1e5b994ea852c1262673208d4c1556a6573e7d9f GIT binary patch literal 153294 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