feat: add note management commands
Extend /note beyond append-only usage with list, show, edit, remove, clear, path, and explicit add subcommands. Keep existing /note <text> behavior compatible, preserve the existing --- separated file format, and number notes only at display time so the stored notes stay clean. Update command help, localization, docs, and tests.
This commit is contained in:
@@ -224,7 +224,7 @@ pub const COMMANDS: &[CommandInfo] = &[
|
||||
CommandInfo {
|
||||
name: "note",
|
||||
aliases: &[],
|
||||
usage: "/note <text>",
|
||||
usage: "/note [add|list|show|edit|remove|clear|path]",
|
||||
description_id: MessageId::CmdNoteDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
|
||||
+339
-20
@@ -1,52 +1,265 @@
|
||||
//! Note command: append to persistent notes file
|
||||
//! Note command: manage persistent workspace notes.
|
||||
|
||||
use crate::tui::app::App;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::CommandResult;
|
||||
|
||||
/// Append a note to the persistent notes file
|
||||
const USAGE: &str = "/note <text> | /note add <text> | /note list | /note show <n> | /note edit <n> <text> | /note remove <n> | /note clear | /note path";
|
||||
|
||||
/// Manage the persistent workspace notes file.
|
||||
pub fn note(app: &mut App, content: Option<&str>) -> CommandResult {
|
||||
let note_content = match content {
|
||||
let input = match content {
|
||||
Some(c) => c.trim(),
|
||||
None => {
|
||||
return CommandResult::error("Usage: /note <text>");
|
||||
return CommandResult::error(format!("Usage: {USAGE}"));
|
||||
}
|
||||
};
|
||||
|
||||
if note_content.is_empty() {
|
||||
if input.is_empty() {
|
||||
return CommandResult::error("Note content cannot be empty");
|
||||
}
|
||||
|
||||
// Determine notes path: workspace/.deepseek/notes.md
|
||||
let notes_path = app.workspace.join(".deepseek").join("notes.md");
|
||||
let notes_path = notes_path(app);
|
||||
let (command, rest) = split_command(input);
|
||||
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = notes_path.parent()
|
||||
&& let Err(e) = fs::create_dir_all(parent)
|
||||
{
|
||||
return CommandResult::error(format!("Failed to create notes directory: {e}"));
|
||||
match command.to_ascii_lowercase().as_str() {
|
||||
"add" => append_note_command(¬es_path, rest),
|
||||
"list" => list_notes_command(¬es_path),
|
||||
"show" => show_note_command(¬es_path, rest),
|
||||
"edit" => edit_note_command(¬es_path, rest),
|
||||
"remove" | "rm" | "delete" => remove_note_command(¬es_path, rest),
|
||||
"clear" => clear_notes_command(¬es_path),
|
||||
"path" => CommandResult::message(format!("Notes path: {}", notes_path.display())),
|
||||
"help" => CommandResult::message(format!("Usage: {USAGE}")),
|
||||
_ => append_note_command(¬es_path, Some(input)),
|
||||
}
|
||||
}
|
||||
|
||||
fn notes_path(app: &App) -> PathBuf {
|
||||
app.workspace.join(".deepseek").join("notes.md")
|
||||
}
|
||||
|
||||
fn split_command(input: &str) -> (&str, Option<&str>) {
|
||||
match input.find(char::is_whitespace) {
|
||||
Some(index) => (&input[..index], Some(input[index..].trim())),
|
||||
None => (input, None),
|
||||
}
|
||||
}
|
||||
|
||||
fn append_note_command(notes_path: &Path, content: Option<&str>) -> CommandResult {
|
||||
let Some(note_content) = content.map(str::trim).filter(|content| !content.is_empty()) else {
|
||||
return CommandResult::error("Usage: /note add <text>");
|
||||
};
|
||||
|
||||
match append_note(notes_path, note_content) {
|
||||
Ok(()) => CommandResult::message(format!("Note appended to {}", notes_path.display())),
|
||||
Err(e) => CommandResult::error(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn list_notes_command(notes_path: &Path) -> CommandResult {
|
||||
let notes = match read_notes(notes_path) {
|
||||
Ok(notes) => notes,
|
||||
Err(e) => return CommandResult::error(e),
|
||||
};
|
||||
|
||||
if notes.is_empty() {
|
||||
return CommandResult::message(format!("No notes found at {}", notes_path.display()));
|
||||
}
|
||||
|
||||
// Append to notes file
|
||||
let mut output = format!("Notes in {}:", notes_path.display());
|
||||
for (index, note) in notes.iter().enumerate() {
|
||||
output.push_str(&format!("\n\n{}. {}", index + 1, note_preview(note)));
|
||||
}
|
||||
CommandResult::message(output)
|
||||
}
|
||||
|
||||
fn show_note_command(notes_path: &Path, rest: Option<&str>) -> CommandResult {
|
||||
let notes = match read_notes(notes_path) {
|
||||
Ok(notes) => notes,
|
||||
Err(e) => return CommandResult::error(e),
|
||||
};
|
||||
let index = match parse_note_index(rest, notes.len(), "/note show <n>") {
|
||||
Ok(index) => index,
|
||||
Err(e) => return CommandResult::error(e),
|
||||
};
|
||||
|
||||
CommandResult::message(format!("Note {}:\n\n{}", index + 1, notes[index]))
|
||||
}
|
||||
|
||||
fn edit_note_command(notes_path: &Path, rest: Option<&str>) -> CommandResult {
|
||||
let Some(rest) = rest else {
|
||||
return CommandResult::error("Usage: /note edit <n> <text>");
|
||||
};
|
||||
let (index_text, new_content) = match split_command(rest) {
|
||||
(index_text, Some(new_content)) if !new_content.trim().is_empty() => {
|
||||
(index_text, new_content.trim())
|
||||
}
|
||||
_ => return CommandResult::error("Usage: /note edit <n> <text>"),
|
||||
};
|
||||
|
||||
let mut notes = match read_notes(notes_path) {
|
||||
Ok(notes) => notes,
|
||||
Err(e) => return CommandResult::error(e),
|
||||
};
|
||||
let index = match parse_note_index(Some(index_text), notes.len(), "/note edit <n> <text>") {
|
||||
Ok(index) => index,
|
||||
Err(e) => return CommandResult::error(e),
|
||||
};
|
||||
|
||||
notes[index] = new_content.to_string();
|
||||
match write_notes(notes_path, ¬es) {
|
||||
Ok(()) => CommandResult::message(format!(
|
||||
"Note {} updated in {}",
|
||||
index + 1,
|
||||
notes_path.display()
|
||||
)),
|
||||
Err(e) => CommandResult::error(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_note_command(notes_path: &Path, rest: Option<&str>) -> CommandResult {
|
||||
let mut notes = match read_notes(notes_path) {
|
||||
Ok(notes) => notes,
|
||||
Err(e) => return CommandResult::error(e),
|
||||
};
|
||||
let index = match parse_note_index(rest, notes.len(), "/note remove <n>") {
|
||||
Ok(index) => index,
|
||||
Err(e) => return CommandResult::error(e),
|
||||
};
|
||||
|
||||
notes.remove(index);
|
||||
match write_notes(notes_path, ¬es) {
|
||||
Ok(()) => CommandResult::message(format!(
|
||||
"Note {} removed from {}",
|
||||
index + 1,
|
||||
notes_path.display()
|
||||
)),
|
||||
Err(e) => CommandResult::error(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_notes_command(notes_path: &Path) -> CommandResult {
|
||||
match write_notes(notes_path, &[]) {
|
||||
Ok(()) => CommandResult::message(format!("Notes cleared in {}", notes_path.display())),
|
||||
Err(e) => CommandResult::error(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn append_note(notes_path: &Path, note_content: &str) -> Result<(), String> {
|
||||
ensure_notes_parent(notes_path)?;
|
||||
|
||||
let mut file = match fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(¬es_path)
|
||||
.open(notes_path)
|
||||
{
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
return CommandResult::error(format!("Failed to open notes file: {e}"));
|
||||
return Err(format!("Failed to open notes file: {e}"));
|
||||
}
|
||||
};
|
||||
|
||||
// Write separator and note content
|
||||
if let Err(e) = writeln!(file, "\n---\n{}", note_content) {
|
||||
return CommandResult::error(format!("Failed to write note: {e}"));
|
||||
return Err(format!("Failed to write note: {e}"));
|
||||
}
|
||||
|
||||
CommandResult::message(format!("Note appended to {}", notes_path.display()))
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_notes(notes_path: &Path) -> Result<Vec<String>, String> {
|
||||
match fs::read_to_string(notes_path) {
|
||||
Ok(content) => Ok(parse_notes(&content)),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
|
||||
Err(e) => Err(format!("Failed to read notes file: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_notes(notes_path: &Path, notes: &[String]) -> Result<(), String> {
|
||||
ensure_notes_parent(notes_path)?;
|
||||
let content = notes
|
||||
.iter()
|
||||
.map(|note| format!("---\n{}", note.trim()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n");
|
||||
fs::write(notes_path, content).map_err(|e| format!("Failed to write notes file: {e}"))
|
||||
}
|
||||
|
||||
fn ensure_notes_parent(notes_path: &Path) -> Result<(), String> {
|
||||
if let Some(parent) = notes_path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| format!("Failed to create notes directory: {e}"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_notes(content: &str) -> Vec<String> {
|
||||
let mut notes = Vec::new();
|
||||
let mut current = Vec::new();
|
||||
let mut saw_separator = false;
|
||||
|
||||
for line in content.lines() {
|
||||
if line.trim() == "---" {
|
||||
if saw_separator || !current.is_empty() {
|
||||
push_note(&mut notes, ¤t);
|
||||
current.clear();
|
||||
}
|
||||
saw_separator = true;
|
||||
} else if saw_separator || !line.trim().is_empty() {
|
||||
current.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
if saw_separator {
|
||||
push_note(&mut notes, ¤t);
|
||||
} else {
|
||||
let trimmed = content.trim();
|
||||
if !trimmed.is_empty() {
|
||||
notes.push(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
notes
|
||||
}
|
||||
|
||||
fn push_note(notes: &mut Vec<String>, lines: &[&str]) {
|
||||
let note = lines.join("\n").trim().to_string();
|
||||
if !note.is_empty() {
|
||||
notes.push(note);
|
||||
}
|
||||
}
|
||||
|
||||
fn note_preview(note: &str) -> String {
|
||||
let first_line = note
|
||||
.lines()
|
||||
.find_map(|line| {
|
||||
let trimmed = line.trim();
|
||||
(!trimmed.is_empty()).then_some(trimmed)
|
||||
})
|
||||
.unwrap_or("(empty note)");
|
||||
if note.lines().filter(|line| !line.trim().is_empty()).count() > 1 {
|
||||
format!("{first_line} ...")
|
||||
} else {
|
||||
first_line.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_note_index(rest: Option<&str>, note_count: usize, usage: &str) -> Result<usize, String> {
|
||||
let Some(index_text) = rest.map(str::trim).filter(|text| !text.is_empty()) else {
|
||||
return Err(format!("Usage: {usage}"));
|
||||
};
|
||||
let index = index_text
|
||||
.parse::<usize>()
|
||||
.map_err(|_| format!("Invalid note number: {index_text}"))?;
|
||||
if index == 0 || index > note_count {
|
||||
return Err(format!(
|
||||
"Note number {index} out of range; there are {note_count} note(s)"
|
||||
));
|
||||
}
|
||||
Ok(index - 1)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -54,6 +267,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::tui::app::{App, TuiOptions};
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn create_test_app_with_tmpdir(tmpdir: &TempDir) -> App {
|
||||
@@ -81,6 +295,14 @@ mod tests {
|
||||
App::new(options, &Config::default())
|
||||
}
|
||||
|
||||
fn notes_path(tmpdir: &TempDir) -> PathBuf {
|
||||
tmpdir.path().join(".deepseek").join("notes.md")
|
||||
}
|
||||
|
||||
fn message(result: CommandResult) -> String {
|
||||
result.message.expect("command message")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_note_without_content_returns_error() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
@@ -105,10 +327,10 @@ mod tests {
|
||||
let mut app = create_test_app_with_tmpdir(&tmpdir);
|
||||
let result = note(&mut app, Some("Test note content"));
|
||||
assert!(result.message.is_some());
|
||||
let msg = result.message.unwrap();
|
||||
let msg = message(result);
|
||||
assert!(msg.contains("Note appended to"));
|
||||
|
||||
let notes_path = tmpdir.path().join(".deepseek").join("notes.md");
|
||||
let notes_path = notes_path(&tmpdir);
|
||||
assert!(notes_path.exists());
|
||||
let content = std::fs::read_to_string(¬es_path).unwrap();
|
||||
assert!(content.contains("Test note content"));
|
||||
@@ -121,11 +343,108 @@ mod tests {
|
||||
note(&mut app, Some("First note"));
|
||||
note(&mut app, Some("Second note"));
|
||||
|
||||
let notes_path = tmpdir.path().join(".deepseek").join("notes.md");
|
||||
let notes_path = notes_path(&tmpdir);
|
||||
let content = std::fs::read_to_string(¬es_path).unwrap();
|
||||
assert!(content.contains("First note"));
|
||||
assert!(content.contains("Second note"));
|
||||
// Should have two separators
|
||||
assert_eq!(content.matches("---").count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_note_list_numbers_entries_without_storing_numbers() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
let mut app = create_test_app_with_tmpdir(&tmpdir);
|
||||
note(&mut app, Some("Alpha note"));
|
||||
note(&mut app, Some("Beta note"));
|
||||
|
||||
let listed = message(note(&mut app, Some("list")));
|
||||
assert!(listed.contains("1. Alpha note"));
|
||||
assert!(listed.contains("2. Beta note"));
|
||||
|
||||
let content = std::fs::read_to_string(notes_path(&tmpdir)).unwrap();
|
||||
assert!(content.contains("Alpha note"));
|
||||
assert!(!content.contains("1. Alpha note"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_note_show_displays_full_multiline_note() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
let mut app = create_test_app_with_tmpdir(&tmpdir);
|
||||
note(&mut app, Some("add first line\nsecond line"));
|
||||
|
||||
let shown = message(note(&mut app, Some("show 1")));
|
||||
assert!(shown.contains("Note 1:"));
|
||||
assert!(shown.contains("first line\nsecond line"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_note_edit_updates_numbered_entry() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
let mut app = create_test_app_with_tmpdir(&tmpdir);
|
||||
note(&mut app, Some("First note"));
|
||||
note(&mut app, Some("Second note"));
|
||||
|
||||
let edited = message(note(&mut app, Some("edit 2 Updated second note")));
|
||||
assert!(edited.contains("Note 2 updated"));
|
||||
|
||||
let content = std::fs::read_to_string(notes_path(&tmpdir)).unwrap();
|
||||
assert!(content.contains("First note"));
|
||||
assert!(content.contains("Updated second note"));
|
||||
assert!(!content.contains("Second note"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_note_remove_renumbers_remaining_entries() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
let mut app = create_test_app_with_tmpdir(&tmpdir);
|
||||
note(&mut app, Some("First note"));
|
||||
note(&mut app, Some("Second note"));
|
||||
note(&mut app, Some("Third note"));
|
||||
|
||||
let removed = message(note(&mut app, Some("remove 2")));
|
||||
assert!(removed.contains("Note 2 removed"));
|
||||
|
||||
let listed = message(note(&mut app, Some("list")));
|
||||
assert!(listed.contains("1. First note"));
|
||||
assert!(listed.contains("2. Third note"));
|
||||
assert!(!listed.contains("Second note"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_note_clear_empties_file() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
let mut app = create_test_app_with_tmpdir(&tmpdir);
|
||||
note(&mut app, Some("First note"));
|
||||
|
||||
let cleared = message(note(&mut app, Some("clear")));
|
||||
assert!(cleared.contains("Notes cleared"));
|
||||
assert_eq!(std::fs::read_to_string(notes_path(&tmpdir)).unwrap(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_note_path_prints_workspace_notes_file() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
let mut app = create_test_app_with_tmpdir(&tmpdir);
|
||||
|
||||
let path = message(note(&mut app, Some("path")));
|
||||
assert!(path.contains(".deepseek"));
|
||||
assert!(path.contains("notes.md"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_note_rejects_out_of_range_index() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
let mut app = create_test_app_with_tmpdir(&tmpdir);
|
||||
note(&mut app, Some("Only note"));
|
||||
|
||||
let result = note(&mut app, Some("show 2"));
|
||||
assert!(result.message.unwrap().contains("out of range"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_notes_handles_plain_text_before_separator() {
|
||||
let parsed = parse_notes("plain note\n---\nseparated note");
|
||||
assert_eq!(parsed, vec!["plain note", "separated note"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,9 +816,7 @@ fn english(id: MessageId) -> &'static str {
|
||||
MessageId::CmdModelDescription => "Switch or view current model",
|
||||
MessageId::CmdModelsDescription => "List available models from API",
|
||||
MessageId::CmdNetworkDescription => "Manage network allow and deny rules",
|
||||
MessageId::CmdNoteDescription => {
|
||||
"Append note to persistent notes file (.deepseek/notes.md)"
|
||||
}
|
||||
MessageId::CmdNoteDescription => "Add, list, edit, or remove workspace notes",
|
||||
MessageId::CmdThemeDescription => "Toggle between dark and light theme",
|
||||
MessageId::CmdProviderDescription => {
|
||||
"Switch or view the active LLM backend (deepseek | nvidia-nim | ollama)"
|
||||
@@ -1157,7 +1155,7 @@ fn japanese(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::CmdModelDescription => "現在のモデルを切り替え・確認",
|
||||
MessageId::CmdModelsDescription => "API から利用可能なモデルを一覧表示",
|
||||
MessageId::CmdNetworkDescription => "ネットワーク許可・拒否ルールを管理",
|
||||
MessageId::CmdNoteDescription => "永続ノートファイル(.deepseek/notes.md)に追記",
|
||||
MessageId::CmdNoteDescription => "ワークスペースノートの追加、一覧、編集、削除",
|
||||
MessageId::CmdThemeDescription => "テーマ(ダーク/ライト)を切り替え",
|
||||
MessageId::CmdProviderDescription => {
|
||||
"現在の LLM バックエンドを切り替え・確認(deepseek | nvidia-nim | ollama)"
|
||||
@@ -1476,7 +1474,7 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::CmdModelDescription => "切换或查看当前模型",
|
||||
MessageId::CmdModelsDescription => "列出 API 中可用的模型",
|
||||
MessageId::CmdNetworkDescription => "管理网络允许和拒绝规则",
|
||||
MessageId::CmdNoteDescription => "将笔记追加到持久笔记文件(.deepseek/notes.md)",
|
||||
MessageId::CmdNoteDescription => "添加、列出、编辑或删除工作区笔记",
|
||||
MessageId::CmdThemeDescription => "在浅色和深色主题之间切换",
|
||||
MessageId::CmdProviderDescription => {
|
||||
"切换或查看当前 LLM 后端(deepseek | nvidia-nim | ollama)"
|
||||
@@ -1781,9 +1779,7 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::CmdModelDescription => "Trocar ou exibir o modelo atual",
|
||||
MessageId::CmdModelsDescription => "Listar os modelos disponíveis pela API",
|
||||
MessageId::CmdNetworkDescription => "Gerenciar regras de rede permitidas e bloqueadas",
|
||||
MessageId::CmdNoteDescription => {
|
||||
"Adicionar nota ao arquivo persistente (.deepseek/notes.md)"
|
||||
}
|
||||
MessageId::CmdNoteDescription => "Adicionar, listar, editar ou remover notas do workspace",
|
||||
MessageId::CmdThemeDescription => "Alternar entre o tema claro e escuro",
|
||||
MessageId::CmdProviderDescription => {
|
||||
"Trocar ou exibir o backend LLM ativo (deepseek | nvidia-nim | ollama)"
|
||||
|
||||
+22
-1
@@ -394,7 +394,7 @@ If you are upgrading from older releases:
|
||||
It is visible in `/config` and can be changed from the TUI. The new path is
|
||||
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.
|
||||
- `notes_path` (string, optional): defaults to `~/.deepseek/notes.txt` and is used by the model-visible `note` tool.
|
||||
- `[memory].enabled` (bool, optional): defaults to `false`. When `true`,
|
||||
the TUI loads the user memory file into a `<user_memory>` prompt block,
|
||||
enables `# foo` quick-capture in the composer, surfaces the `/memory`
|
||||
@@ -463,6 +463,27 @@ If you are upgrading from older releases:
|
||||
- `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`).
|
||||
- `features.*` (optional): feature flag overrides (see below).
|
||||
|
||||
### Workspace notes
|
||||
|
||||
`/note` manages a simple notes file in the current workspace at
|
||||
`.deepseek/notes.md`. Existing `/note <text>` usage still appends a note.
|
||||
The management forms are:
|
||||
|
||||
| Command | Action |
|
||||
|---|---|
|
||||
| `/note <text>` | Append a note (legacy shorthand) |
|
||||
| `/note add <text>` | Append a note explicitly |
|
||||
| `/note list` | List notes with temporary 1-based numbers |
|
||||
| `/note show <n>` | Show the full note at number `n` |
|
||||
| `/note edit <n> <text>` | Replace note `n` with new text |
|
||||
| `/note remove <n>` | Delete note `n`; `rm` and `delete` are aliases |
|
||||
| `/note clear` | Empty the workspace notes file |
|
||||
| `/note path` | Show the resolved workspace notes path |
|
||||
|
||||
The numbers shown by `/note list` are not stored in the file; they are derived
|
||||
from the current order each time notes are read. This keeps the file format
|
||||
compatible with the existing `---`-separated notes.
|
||||
|
||||
### User memory
|
||||
|
||||
User memory is split across one top-level path setting and one opt-in
|
||||
|
||||
Reference in New Issue
Block a user