diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ac41cdf..81805435 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.9] - Unreleased + +### Changed +- **User memory docs + help polish** (#497) — `/memory` is now listed in + slash-command help, supports `/memory help`, and the README / + configuration docs now point at the full `docs/MEMORY.md` guide and + document both `[memory].enabled` and `DEEPSEEK_MEMORY`. + ## [0.8.8] - 2026-05-03 ### Added diff --git a/README.md b/README.md index fb57e62d..67ce92a1 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,7 @@ A stabilization-focused release: a thick band of UX polish on top of the v0.8.6 - **Every `HookEvent` now has a live producer** — `tool_call_before` / `tool_call_after` / `message_submit` / `on_error` fire from the runtime in addition to the existing session-lifecycle and mode-change events. Hooks remain read-only observers in v0.8.8 ([#455](https://github.com/Hmbown/DeepSeek-TUI/issues/455)). - **`instructions = [...]` config array** lets you stack additional system-prompt files; paths capped at 100 KiB each, project array replaces user array wholesale ([#454](https://github.com/Hmbown/DeepSeek-TUI/issues/454)). - **`deepseek pr ` subcommand** fetches a PR's title / body / diff via `gh` and launches the TUI with a review prompt already in the composer. Codepoint-safe diff cap at 200 KiB; optional `--repo` / `--checkout` ([#451](https://github.com/Hmbown/DeepSeek-TUI/issues/451)). -- **User-memory MVP** (opt-in) — `~/.deepseek/memory.md` injected into the system prompt as a `` block; `# foo` typed in the composer appends a timestamped bullet without firing a turn; `/memory [show|path|clear|edit]` for inspection. Default off; enable with `[memory] enabled = true` or `DEEPSEEK_MEMORY=on` ([#489](https://github.com/Hmbown/DeepSeek-TUI/issues/489)–[#493](https://github.com/Hmbown/DeepSeek-TUI/issues/493)). +- **User-memory MVP** (opt-in) — `~/.deepseek/memory.md` injected into the system prompt as a `` block; `# foo` typed in the composer appends a timestamped bullet without firing a turn; `/memory [show|path|clear|edit|help]` for inspection. Default off; enable with `[memory] enabled = true` or `DEEPSEEK_MEMORY=on`. See [docs/MEMORY.md](docs/MEMORY.md) for the full guide and examples ([#489](https://github.com/Hmbown/DeepSeek-TUI/issues/489)–[#493](https://github.com/Hmbown/DeepSeek-TUI/issues/493)). ### 🔒 Security diff --git a/crates/tui/src/commands/core.rs b/crates/tui/src/commands/core.rs index 1f222c11..9e93e3ad 100644 --- a/crates/tui/src/commands/core.rs +++ b/crates/tui/src/commands/core.rs @@ -346,6 +346,16 @@ mod tests { assert!(msg.contains("Aliases: dashboard, api")); } + #[test] + fn test_help_memory_topic_shows_usage_and_description() { + let mut app = create_test_app(); + let result = help(&mut app, Some("memory")); + let msg = result.message.expect("help topic should return message"); + assert!(msg.contains("memory")); + assert!(msg.contains("persistent user-memory file")); + assert!(msg.contains("Usage: /memory [show|path|clear|edit|help]")); + } + #[test] fn test_help_pushes_overlay() { let mut app = create_test_app(); diff --git a/crates/tui/src/commands/memory.rs b/crates/tui/src/commands/memory.rs index def6b19c..78bd4480 100644 --- a/crates/tui/src/commands/memory.rs +++ b/crates/tui/src/commands/memory.rs @@ -9,6 +9,7 @@ //! - `/memory show` — alias for the no-arg form //! - `/memory clear` — replace the file contents with an empty marker //! - `/memory path` — show only the resolved path +//! - `/memory help` — show command-specific help and the resolved path //! //! Editor integration (`/memory edit`) is intentionally minimal: the //! command prints a copy-pasteable shell line to open the file in the @@ -17,10 +18,31 @@ //! doesn't have access to. use std::fs; +use std::path::Path; use super::CommandResult; use crate::tui::app::App; +const MEMORY_USAGE: &str = "/memory [show|path|clear|edit|help]"; + +fn memory_help(path: &Path) -> String { + format!( + "Inspect or manage your persistent user-memory file.\n\n\ + Usage: {MEMORY_USAGE}\n\n\ + Current path: {}\n\n\ + Subcommands:\n\ + /memory Show the resolved path and current contents\n\ + /memory show Alias for the no-arg form\n\ + /memory path Print just the resolved path\n\ + /memory clear Replace the file contents with an empty marker\n\ + /memory edit Print the editor command for this file\n\ + /memory help Show this help\n\n\ + Quick capture: type `# foo` in the composer to append a timestamped\n\ + bullet without firing a turn.", + path.display() + ) +} + pub fn memory(app: &mut App, arg: Option<&str>) -> CommandResult { if !app.use_memory { return CommandResult::error( @@ -55,8 +77,76 @@ pub fn memory(app: &mut App, arg: Option<&str>) -> CommandResult { "to edit your memory file, run:\n\n ${{VISUAL:-${{EDITOR:-vi}}}} {}", path.display() )), + "help" => CommandResult::message(memory_help(&path)), _ => CommandResult::error(format!( - "unknown subcommand `{sub}`. usage: /memory [show|path|clear|edit]" + "unknown subcommand `{sub}`. Try `/memory help`.\n\n{}", + memory_help(&path) )), } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::Config; + use crate::tui::app::{App, TuiOptions}; + use tempfile::TempDir; + + fn create_test_app_with_memory(tmpdir: &TempDir, use_memory: bool) -> App { + let options = TuiOptions { + model: "deepseek-v4-pro".to_string(), + workspace: tmpdir.path().to_path_buf(), + config_path: None, + config_profile: None, + allow_shell: false, + use_alt_screen: true, + use_mouse_capture: false, + use_bracketed_paste: true, + max_subagents: 1, + skills_dir: tmpdir.path().join("skills"), + memory_path: tmpdir.path().join("memory.md"), + notes_path: tmpdir.path().join("notes.txt"), + mcp_config_path: tmpdir.path().join("mcp.json"), + use_memory, + start_in_agent_mode: false, + skip_onboarding: true, + yolo: false, + resume_session_id: None, + initial_input: None, + }; + App::new(options, &Config::default()) + } + + #[test] + fn memory_help_lists_subcommands_and_resolved_path() { + let tmpdir = TempDir::new().expect("tempdir"); + let mut app = create_test_app_with_memory(&tmpdir, true); + let result = memory(&mut app, Some("help")); + let msg = result.message.expect("help should return text"); + assert!(msg.contains("Usage: /memory [show|path|clear|edit|help]")); + assert!(msg.contains("/memory edit")); + assert!(msg.contains(app.memory_path.to_string_lossy().as_ref())); + } + + #[test] + fn memory_unknown_subcommand_points_to_help() { + let tmpdir = TempDir::new().expect("tempdir"); + let mut app = create_test_app_with_memory(&tmpdir, true); + let result = memory(&mut app, Some("wat")); + let msg = result + .message + .expect("unknown subcommand should return text"); + assert!(msg.contains("Try `/memory help`")); + assert!(msg.contains("/memory clear")); + } + + #[test] + fn memory_disabled_returns_enablement_hint() { + let tmpdir = TempDir::new().expect("tempdir"); + let mut app = create_test_app_with_memory(&tmpdir, false); + let result = memory(&mut app, None); + let msg = result.message.expect("disabled memory should return text"); + assert!(msg.contains("user memory is disabled")); + assert!(msg.contains("DEEPSEEK_MEMORY=on")); + } +} diff --git a/crates/tui/src/commands/mod.rs b/crates/tui/src/commands/mod.rs index 9a906cac..cdfcb3f6 100644 --- a/crates/tui/src/commands/mod.rs +++ b/crates/tui/src/commands/mod.rs @@ -210,6 +210,12 @@ pub const COMMANDS: &[CommandInfo] = &[ usage: "/note ", description_id: MessageId::CmdNoteDescription, }, + CommandInfo { + name: "memory", + aliases: &[], + usage: "/memory [show|path|clear|edit|help]", + description_id: MessageId::CmdMemoryDescription, + }, CommandInfo { name: "attach", aliases: &["image", "media"], @@ -812,6 +818,7 @@ mod tests { fn command_registry_contains_config_and_links_but_not_set_or_deepseek() { assert!(COMMANDS.iter().any(|cmd| cmd.name == "config")); assert!(COMMANDS.iter().any(|cmd| cmd.name == "links")); + assert!(COMMANDS.iter().any(|cmd| cmd.name == "memory")); assert!(!COMMANDS.iter().any(|cmd| cmd.name == "set")); assert!(!COMMANDS.iter().any(|cmd| cmd.name == "deepseek")); } diff --git a/crates/tui/src/localization.rs b/crates/tui/src/localization.rs index 2e936f67..147ee22d 100644 --- a/crates/tui/src/localization.rs +++ b/crates/tui/src/localization.rs @@ -238,6 +238,7 @@ pub enum MessageId { CmdLoadDescription, CmdLogoutDescription, CmdMcpDescription, + CmdMemoryDescription, CmdModelDescription, CmdModelsDescription, CmdNoteDescription, @@ -424,6 +425,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[ MessageId::CmdLoadDescription, MessageId::CmdLogoutDescription, MessageId::CmdMcpDescription, + MessageId::CmdMemoryDescription, MessageId::CmdModelDescription, MessageId::CmdModelsDescription, MessageId::CmdNoteDescription, @@ -739,6 +741,7 @@ fn english(id: MessageId) -> &'static str { MessageId::CmdLoadDescription => "Load session from file", MessageId::CmdLogoutDescription => "Clear API key and return to setup", MessageId::CmdMcpDescription => "Open or manage MCP servers", + MessageId::CmdMemoryDescription => "Inspect or manage the persistent user-memory file", MessageId::CmdModelDescription => "Switch or view current model", MessageId::CmdModelsDescription => "List available models from API", MessageId::CmdNoteDescription => { @@ -1020,6 +1023,7 @@ fn japanese(id: MessageId) -> Option<&'static str> { MessageId::CmdLoadDescription => "ファイルからセッションを読み込み", MessageId::CmdLogoutDescription => "API キーを消去してセットアップに戻る", MessageId::CmdMcpDescription => "MCP サーバを開く・管理する", + MessageId::CmdMemoryDescription => "永続ユーザーメモリファイルを確認・管理", MessageId::CmdModelDescription => "現在のモデルを切り替え・確認", MessageId::CmdModelsDescription => "API から利用可能なモデルを一覧表示", MessageId::CmdNoteDescription => "永続ノートファイル(.deepseek/notes.md)に追記", @@ -1281,6 +1285,7 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> { MessageId::CmdLoadDescription => "从文件加载会话", MessageId::CmdLogoutDescription => "清除 API 密钥并返回设置", MessageId::CmdMcpDescription => "打开或管理 MCP 服务器", + MessageId::CmdMemoryDescription => "查看或管理持久用户记忆文件", MessageId::CmdModelDescription => "切换或查看当前模型", MessageId::CmdModelsDescription => "列出 API 中可用的模型", MessageId::CmdNoteDescription => "将笔记追加到持久笔记文件(.deepseek/notes.md)", @@ -1530,6 +1535,9 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> { MessageId::CmdLoadDescription => "Carregar a sessão de um arquivo", MessageId::CmdLogoutDescription => "Limpar a chave de API e voltar à configuração", MessageId::CmdMcpDescription => "Abrir ou gerenciar servidores MCP", + MessageId::CmdMemoryDescription => { + "Inspecionar ou gerenciar o arquivo persistente de memória do usuário" + } MessageId::CmdModelDescription => "Trocar ou exibir o modelo atual", MessageId::CmdModelsDescription => "Listar os modelos disponíveis pela API", MessageId::CmdNoteDescription => { diff --git a/crates/tui/src/memory.rs b/crates/tui/src/memory.rs index f2158254..8a61b6ef 100644 --- a/crates/tui/src/memory.rs +++ b/crates/tui/src/memory.rs @@ -9,7 +9,9 @@ //! prompt alongside the existing `` block. //! - **`# foo`** typed in the composer appends `foo` to the memory //! file as a timestamped bullet — fast capture without leaving the TUI. -//! - **`/memory`** opens the memory file in `$VISUAL` / `$EDITOR`. +//! - **`/memory`** shows the resolved file path and current contents, and +//! **`/memory edit`** prints a copy-pasteable `$VISUAL` / `$EDITOR` +//! command for opening the file yourself. //! - **`remember` tool** lets the model itself append a bullet when it //! notices a durable preference or convention worth keeping across //! sessions. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index b8e1b87c..f5aea250 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -128,6 +128,7 @@ These override config values: - `DEEPSEEK_SKILLS_DIR` - `DEEPSEEK_MCP_CONFIG` - `DEEPSEEK_NOTES_PATH` +- `DEEPSEEK_MEMORY` (`1|on|true|yes|y|enabled` turns user memory on) - `DEEPSEEK_MEMORY_PATH` - `DEEPSEEK_ALLOW_SHELL` (`1`/`true` enables) - `DEEPSEEK_APPROVAL_POLICY` (`on-request|untrusted|never`) @@ -320,6 +321,11 @@ If you are upgrading from older releases: used immediately by `/mcp`, but rebuilding the model-visible MCP tool pool requires restarting the TUI. - `notes_path` (string, optional): defaults to `~/.deepseek/notes.txt` and is used by the `note` tool. +- `[memory].enabled` (bool, optional): defaults to `false`. When `true`, + the TUI loads the user memory file into a `` prompt block, + enables `# foo` quick-capture in the composer, surfaces the `/memory` + slash command, and registers the `remember` tool. The same toggle is + available via `DEEPSEEK_MEMORY=on`. - `memory_path` (string, optional): defaults to `~/.deepseek/memory.md`. Used by the user-memory feature when enabled — see [`MEMORY.md`](MEMORY.md) for the full feature surface (`# foo` @@ -369,6 +375,31 @@ If you are upgrading from older releases: - `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`). - `features.*` (optional): feature flag overrides (see below). +### User memory + +User memory is split across one top-level path setting and one opt-in +toggle table: + +```toml +memory_path = "~/.deepseek/memory.md" + +[memory] +enabled = true +``` + +Notes: + +- `memory_path` stays at the top level beside `notes_path` and + `skills_dir`; it is not nested under `[memory]`. +- `DEEPSEEK_MEMORY_PATH` overrides the file path from the environment. +- `DEEPSEEK_MEMORY=on` (also `1`, `true`, `yes`, `y`, or `enabled`) + flips the feature on without editing `config.toml`. +- The feature is inert when disabled: no file is injected, `# foo` + falls through to normal message submission, and the model does not + see the `remember` tool. +- See [`MEMORY.md`](MEMORY.md) for examples and the full `/memory` + command surface. + ### Parsed but currently unused (reserved for future versions) These keys are accepted by the config loader but not currently used by the interactive TUI or built-in tools: diff --git a/docs/MEMORY.md b/docs/MEMORY.md index 2cdc7cc6..e3e0842a 100644 --- a/docs/MEMORY.md +++ b/docs/MEMORY.md @@ -20,6 +20,9 @@ Either set the env var: export DEEPSEEK_MEMORY=on ``` +Accepted truthy values are `1`, `on`, `true`, `yes`, `y`, and +`enabled`. + …or add to `~/.deepseek/config.toml`: ```toml @@ -31,7 +34,25 @@ Restart the TUI after toggling. Disabling is the same in reverse. The memory file lives at `~/.deepseek/memory.md` by default; override with `memory_path` in `config.toml` or `DEEPSEEK_MEMORY_PATH` in -the environment. +the environment. `DEEPSEEK_MEMORY_PATH` wins over the config file when +both are set. + +## Quick examples + +```text +# remember that this repo prefers cargo fmt before commits +/memory +/memory path +/memory edit +/memory help +``` + +- Type `# remember that this repo prefers cargo fmt before commits` in + the composer to append a timestamped bullet without firing a turn. +- Run `/memory` to confirm where the feature is writing and what is + currently stored. +- Run `/memory edit` when you want to groom the file manually in your + editor. ## What gets injected @@ -84,11 +105,18 @@ Inspect, clear, or get hints about editing the file: | `/memory path` | Print just the resolved path | | `/memory clear` | Replace the file with an empty marker | | `/memory edit` | Print the `${VISUAL:-${EDITOR:-vi}} ` shell line | +| `/memory help` | Show command-specific help and the current path | The `/memory edit` form intentionally just prints the command rather than spawning the editor in-process — that keeps the slash-command handler simple and consistent regardless of which editor you use. +You can also discover the feature from the general help surfaces: + +- `/help memory` shows the slash-command summary and usage line. +- `/memory help` prints the memory-specific subcommands plus the + resolved path. + ### 3. The `remember` tool (auto-update, #489) When memory is enabled the model gets a `remember` tool with this @@ -134,6 +162,22 @@ about the timestamp format; it just reads the whole file as the memory block. The timestamp is convention so you can tell when each note was added when grooming the file. +## Hierarchy and imports + +Memory is intentionally **user-scoped** rather than repo-scoped. It +sits alongside — not inside — project instruction sources such as +`AGENTS.md`, `.deepseek/instructions.md`, and `instructions = [...]`. + +- Use **memory** for durable personal preferences that should follow + you across repos and sessions. +- Use **project instructions** for repo-specific conventions that + should travel with the codebase. + +The memory loader currently reads one resolved file path verbatim. +`@path` imports / includes are **not** supported today; if you need a +larger reusable instruction bundle, put it in a project instruction +file or a skill instead. + ## What stays out of memory Memory is for **durable** signal. Things that should NOT live there: