Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions docs/user/cli-tui.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,6 @@ echo "your prompt" | memo

此外,直接输入 `exit`(不加 `/`)也可退出。

### 本地 Shell 命令

输入以 `$` 开头的命令会在本地 `cwd` 执行:

```text
$ git status
$ rg "createAgentSession" -n
```

这是用户主动执行,不走模型工具审批流程。

## 快捷键

- `Enter`:发送
Expand Down
2 changes: 2 additions & 0 deletions docs/user/sessions-history.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Memo 会把会话过程写入本地 JSONL,便于恢复上下文和排查问题

排障时通常重点看:`action`、`observation`、`final`。

补充:`session_start` 事件会写入本次会话实际使用的系统提示词(`role=system`,`content` 字段)。

## 三、如何恢复历史

在输入框输入:
Expand Down
42 changes: 0 additions & 42 deletions packages/cli/src/tui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { readFile, stat } from 'node:fs/promises'
import { basename } from 'node:path'
import { randomUUID } from 'node:crypto'
import { exec } from 'node:child_process'
import { promisify } from 'node:util'
import { Box, useApp, Text } from 'ink'
import {
createAgentSession,
Expand All @@ -29,8 +27,6 @@ import { resolveSlashCommand } from './commands'
import { checkForUpdate, findLocalPackageInfoSync } from './version'
import { SetupWizard } from './components/setup/SetupWizard'

const execAsync = promisify(exec)

export type AppProps = {
sessionOptions: AgentSessionOptions
providerName: string
Expand Down Expand Up @@ -453,32 +449,6 @@ export function App({
[appendSystemMessage, busy, currentModel, currentProvider, persistCurrentProvider],
)

const runShellCommand = useCallback(
async (command: string) => {
if (!command.trim()) {
appendSystemMessage('Shell Command', 'Usage: $ <command> (e.g. $ git status)')
return
}
setBusy(true)
try {
const { stdout, stderr } = await execAsync(command, {
cwd,
maxBuffer: 5 * 1024 * 1024, // prevent truncation on large outputs
})
const output = [stdout?.trim(), stderr?.trim()].filter(Boolean).join('\n')
appendSystemMessage('Shell Result', output || '(no output)')
} catch (err) {
const error = err as Error & { stdout?: string; stderr?: string }
const parts = [error.stdout?.trim(), error.stderr?.trim(), error.message]
const message = parts.filter(Boolean).join('\n')
appendSystemMessage('Shell Error', message || 'Command failed')
} finally {
setBusy(false)
}
},
[appendSystemMessage, cwd],
)

const handleCommand = useCallback(
async (raw: string) => {
const result = resolveSlashCommand(raw, {
Expand Down Expand Up @@ -542,10 +512,6 @@ Make the AGENTS.md concise but informative, following best practices for AI agen
}
return
}
if (result.kind === 'shell_command') {
await runShellCommand(result.command)
return
}
appendSystemMessage(result.title, result.content)
},
[
Expand All @@ -560,7 +526,6 @@ Make the AGENTS.md concise but informative, following best practices for AI agen
currentProvider,
contextLimit,
providersState,
runShellCommand,
],
)

Expand Down Expand Up @@ -601,13 +566,6 @@ Make the AGENTS.md concise but informative, following best practices for AI agen
}

if (!session || busy) return
const trimmed = value.trim()
if (trimmed.startsWith('$')) {
const command = trimmed.slice(1).trim()
setInputHistory((prev) => [...prev, value])
await runShellCommand(command)
return
}
if (value.startsWith('/')) {
await handleCommand(value)
return
Expand Down
29 changes: 29 additions & 0 deletions packages/cli/src/tui/commands.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import assert from 'node:assert'
import { describe, test } from 'vitest'
import { resolveSlashCommand } from './commands'

const context = {
configPath: '/tmp/config.toml',
providerName: 'test-provider',
model: 'test-model',
mcpServers: {},
providers: [],
contextLimit: 120000,
}

describe('resolveSlashCommand', () => {
test('treats /$ command as unknown', () => {
const result = resolveSlashCommand('/$ git status', context)
assert.strictEqual(result.kind, 'message')
assert.strictEqual(result.title, 'Unknown')
assert.ok(result.content.includes('Unknown command: /$ git status'))
})

test('help does not include shell execution command', () => {
const result = resolveSlashCommand('/help', context)
assert.strictEqual(result.kind, 'message')
assert.strictEqual(result.title, 'Help')
assert.ok(!result.content.includes('Execute shell command'))
assert.ok(!result.content.includes('\n $'))
})
})
12 changes: 0 additions & 12 deletions packages/cli/src/tui/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export type SlashCommandResult =
| { kind: 'switch_model'; provider: ProviderConfig }
| { kind: 'set_context_limit'; limit: number }
| { kind: 'init_agents_md' }
| { kind: 'shell_command'; command: string }

export function resolveSlashCommand(raw: string, context: SlashResolveContext): SlashCommandResult {
const [commandRaw, ...rest] = raw.trim().slice(1).split(/\s+/)
Expand Down Expand Up @@ -77,17 +76,6 @@ export function resolveSlashCommand(raw: string, context: SlashResolveContext):
}
case 'init':
return { kind: 'init_agents_md' }
case '$': {
const command = rest.join(' ').trim()
if (!command) {
return {
kind: 'message',
title: 'Shell Command',
content: 'Usage: $ <command> (e.g. $ git status)',
}
}
return { kind: 'shell_command', command }
}
case 'models':
if (!context.providers.length) {
return {
Expand Down
1 change: 0 additions & 1 deletion packages/cli/src/tui/slash/help_text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const SLASH_LINES = buildSlashHelpLines().join('\n')
export const HELP_TEXT = `Available commands:
${SLASH_LINES}
exit Exit the session (no slash)
$ Execute shell command (e.g. $ git status)

Shortcuts:
Enter Send message
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/runtime/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,11 @@ class AgentSessionImpl implements AgentSession {
const effectiveMaxPromptTokens = this.options.maxPromptTokens ?? DEFAULT_MAX_PROMPT_TOKENS

if (!this.sessionStartEmitted) {
const systemPrompt =
this.history[0]?.role === 'system' ? this.history[0].content : undefined
await this.emitEvent('session_start', {
content: systemPrompt,
role: systemPrompt ? 'system' : undefined,
meta: {
mode: this.mode,
cwd: process.cwd(),
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/runtime/session_hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,11 @@ describe('session hooks & middleware', () => {
const sessionStart = events.find((event) => event.type === 'session_start')
assert.ok(sessionStart, 'session_start should exist')
assert.strictEqual(sessionStart?.meta?.cwd, process.cwd())
assert.strictEqual(sessionStart?.role, 'system')
assert.ok(
typeof sessionStart?.content === 'string' && sessionStart.content.length > 0,
'session_start should include system prompt content',
)

const actionEvent = events.find((event) => event.type === 'action')
assert.ok(actionEvent, 'action event should exist')
Expand Down