10.1.15 → `10.001.0015`) |Ctrl+P |syn list, syn search (Frontmatter) | SQLite |syn search --content (全文) | Ripgrep → 再查 SQLite 补充元数据 |syn show, syn edit | 实际 Markdown 文件 |
1. Ripgrep 搜索全文 → 返回匹配文件路径列表
2. 用路径列表查询 SQLite → 过滤 Status=Todo
3. 返回最终结果(带完整元数据)
# 混合搜索:全文包含 "kubernetes" 且状态为 todo
syn search "kubernetes" --status todo
# 混合搜索:全文包含 "设计" 且标签包含 urgent
syn search "设计" --tag urgent
# 纯元数据搜索(due-before 基于 due 字段)
syn search --type task --priority high --due-before 2024-01-15
--due-before | due | 截止日期早于指定日期的任务 |--due-after | due | 截止日期晚于指定日期的任务 |--created-before | created | 创建时间早于指定日期 |--created-after | created | 创建时间晚于指定日期 |--modified-before | modified | 修改时间早于指定日期 |--modified-after | modified | 修改时间晚于指定日期 |
# 查找本周内到期的任务
syn search --type task --due-after monday --due-before sunday
# 查找过去 7 天修改过的笔记
syn search --type note --modified-after "7 days ago"
# ❌ 错误:单条插入 1000 次 ≈ 5秒
for item in items:
cursor.execute("INSERT INTO resources ...", item)
conn.commit()
# ✅ 正确:事务批量插入 1000 次 ≈ 0.05秒
cursor.execute("BEGIN TRANSACTION")
for item in items:
cursor.execute("INSERT INTO resources ...", item)
cursor.execute("COMMIT")
from textual import work
class SynapseApp(App):
@work(exclusive=True)
async def action_batch_update_tags(self, ids: list, add_tag: str):
"""耗时操作放入后台线程,不阻塞 UI"""
self.notify("正在批量更新标签...")
# 执行文件 IO 和 DB 更新
await processor.batch_add_tag(ids, add_tag)
self.notify(f"✓ 已更新 {len(ids)} 个资源")
self.refresh_list()
syn index rebuild 重建 |.gitignore 排除,`syn media` 上传到外部存储 |syn graph 接口
# config.yaml
email:
provider: gmail # gmail | imap | microsoft
gmail:
client_id_env: GMAIL_CLIENT_ID
client_secret_env: GMAIL_CLIENT_SECRET
imap:
server: imap.example.com
port: 993
username_env: IMAP_USERNAME
password_env: IMAP_PASSWORD
# ~/.config/synapse/config.yaml
workspace: ~/synapse-data
default_namespace: personal
namespaces:
- personal # 1
- work-project # 2
- study # 3
editor:
windows: { default: notepad, options: [notepad, typora, code] }
linux: { default: vim, options: [vim, typora] }
macos: { default: vim, options: [vim, typora] }
git:
auto_commit: true
commit_style: conventional # conventional | simple | llm
llm:
enabled: false
provider: litellm
model: gpt-4o-mini
api_base: https://api.openai.com/v1
api_key_env: OPENAI_API_KEY
remotes:
- name: origin
url: [email protected]:user/synapse-data.git
auto_push: true
johnny_decimal:
areas:
00: "Inbox"
10: "个人管理"
20: "工作项目"
30: "学习成长"
review:
daily_time: "08:00"
weekly_day: friday
weekly_time: "18:00"
-- 主索引表
CREATE TABLE resources (
id TEXT PRIMARY KEY, -- "10.001.0015"
namespace TEXT,
area TEXT, -- "10"
category TEXT, -- "001"
type TEXT,
title TEXT,
status TEXT,
priority TEXT,
due DATE,
created DATETIME,
modified DATETIME,
tags TEXT, -- JSON array
content_hash TEXT, -- 用于变更检测
file_path TEXT
);
-- 全文搜索 (FTS5) - 存储预分词后的内容
CREATE VIRTUAL TABLE resources_fts USING fts5(
id, title_tokens, tags_tokens, content_tokens,
tokenize = 'simple' -- 使用默认分词器,输入已分词字符串
);
import jieba
import re
# 停用词表(标点、空格、无意义字符)
STOPWORDS = set(['的', '了', '是', '在', '我', '有', '和', '就',
'不', '人', '都', '一', '一个', '上', '也', '很',
'到', '说', '要', '去', '你', '会', '着', '没有'])
class SearchEngine:
def _tokenize(self, text: str) -> str:
"""分词 + 停用词过滤 + 标点清理"""
# 1. 移除标点符号和特殊字符
text = re.sub(r'[^\w\s\u4e00-\u9fff]', ' ', text)
# 2. jieba 分词
tokens = jieba.cut_for_search(text)
# 3. 停用词过滤 + 去除空白
filtered = [t.strip() for t in tokens
if t.strip() and t not in STOPWORDS]
return " ".join(filtered)
def update_index(self, id: str, title: str, content: str):
"""写入索引前,用 jieba 预处理 + 停用词过滤"""
title_tokens = self._tokenize(title)
content_tokens = self._tokenize(content)
cursor.execute("""
INSERT INTO resources_fts (id, title_tokens, content_tokens)
VALUES (?, ?, ?)
""", (id, title_tokens, content_tokens))
def search(self, query: str):
"""搜索时,同样分词处理"""
query_tokens = " AND ".join(self._tokenize(query).split())
cursor.execute("""
SELECT id FROM resources_fts
WHERE content_tokens MATCH ?
""", (query_tokens,))
index.db 体积(去除 30%+ 无意义词)syn 启动 | 检测 Git 变更,增量更新索引 |post-merge, post-checkout 触发重建 |syn index rebuild 强制重建 |# config.yaml
media:
provider: s3 # s3 | github | local
# 凭证配置
credentials:
# 方式 1:环境变量(推荐)
aws_access_key_env: AWS_ACCESS_KEY_ID
aws_secret_key_env: AWS_SECRET_ACCESS_KEY
# 方式 2:配置文件(不推荐,敏感信息)
# aws_access_key: "AKIA..."
# aws_secret_key: "..."
s3:
bucket: your-bucket
region: us-east-1
prefix: synapse/ # 存储前缀
# 最终路径:s3://your-bucket/synapse/{resource_id}/{filename}
github:
repo: user/media
branch: main
token_env: GITHUB_TOKEN # GitHub Personal Access Token
local:
path: ~/synapse-media # 本地存储路径(.gitignore 隔离)
base_url: file://~/synapse-media
Ctrl+V`:检测剪贴板图片 → 自动调用 `syn media paste → 插入链接
# 本地执行,立即生成并打开
syn review daily # → 生成今日回顾,直接打开编辑
syn review weekly # → 生成周报,直接打开编辑
syn review monthly # → 生成月报,直接打开编辑
syn standup # → 生成站会报告
# LLM 辅助生成(可选)
syn review daily --llm # → 使用 LLM 生成摘要
syn review weekly --llm # → 使用 LLM 生成周报
syn review monthly --llm # → 使用 LLM 生成月报
personal/
└── 90/ # Area 90: 回顾与复盘
├── 001/ # Category 001: 每日回顾
│ ├── 90.001.0001-note-2024-01-15-daily-review.md
│ └── 90.001.0002-note-2024-01-16-daily-review.md
├── 002/ # Category 002: 每周回顾
│ └── 90.002.0001-note-2024-W03-weekly-review.md
├── 003/ # Category 003: 每月回顾
│ └── 90.003.0001-note-2024-01-monthly-review.md
└── 004/ # Category 004: 站会报告
└── 90.004.0001-note-2024-01-15-standup.md
---
jd_number: "90.001.0015"
type: note
namespace: personal
title: "2024-01-15 每日回顾"
tags: [review, daily]
created: 2024-01-15T18:00:00Z
modified: 2024-01-15T18:30:00Z
review_type: daily # daily | weekly | monthly | standup
review_period: "2024-01-15"
llm_generated: false # 是否使用 LLM 生成
---
# config.yaml
hooks:
review_daily:
command: "python ~/.synapse/hooks/daily_review.py"
auto_open: true # 生成后自动打开编辑器
review_weekly:
command: "python ~/.synapse/hooks/weekly_review.py"
auto_open: true
review_monthly:
command: "python ~/.synapse/hooks/monthly_review.py"
auto_open: true
standup:
command: "python ~/.synapse/hooks/standup.py"
auto_open: true
review:
area: "90" # Review 存储的 Area
daily_category: "001" # syn review daily
weekly_category: "002" # syn review weekly
monthly_category: "003" # syn review monthly
standup_category: "004" # syn standup
llm:
enabled: true
prompt_template: "~/.synapse/prompts/review.md"
syn graph # 启动图谱视图(TUI)
syn graph export # 导出图谱数据
syn graph sync # 同步到 Nebula Graph(如配置)
syn new email --subject "项目进度汇报"
# config.yaml
git:
merge_strategy:
# 默认策略:取 modified 最新的一方 (Last Write Wins)
default: "take_latest_modified"
# 特殊字段策略
fields:
# 标签:取并集。A端加了[urgent], B端加了[work] -> [urgent, work]
tags: "union"
# 关联:取并集
related: "union"
# 状态:取最新修改方(或自定义优先级:done > in-progress > todo)
status: "take_latest_modified"
# 正文内容:标准 Git 文本合并
content: "git_standard"
.conflict 备份文件syn edit 前:记录文件 hashwatchdog 库监听文件系统事件,TUI 运行时实时感知外部修改:
# 伪代码示意 - 注意线程安全!
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
class SynapseFileWatcher(FileSystemEventHandler):
def __init__(self, app):
self.app = app # Textual App 实例
def on_modified(self, event):
if event.src_path == self.app.current_open_file:
# ⚠️ 关键:watchdog 在独立线程,必须用 call_from_thread
self.app.call_from_thread(
self.app.show_file_modified_warning,
event.src_path
)
watchdog 运行在独立线程app.call_from_thread() 跨线程调用,否则 TUI 会崩溃git pull 或 git checkout 时,文件系统变化会触发 watchdog,错误地向用户报告"外部修改警告"。
class FileWatcher:
def __init__(self):
self._suspended = False
def pause(self):
"""暂停监听(Git 操作前调用)"""
self._suspended = True
def resume(self):
"""恢复监听(Git 操作后调用)"""
self._suspended = False
def on_modified(self, event):
if self._suspended:
return # 忽略自身触发的变更
# ... 正常处理外部修改
class SynapseApp:
def perform_git_operation(self):
self.file_watcher.pause() # 暂停监听
try:
git.pull()
self.index.refresh() # 主动刷新索引
finally:
self.file_watcher.resume() # 恢复监听
class SynapseFileWatcher:
def __init__(self, app):
self.app = app
self.observer = Observer()
self._current_path = None
self._started = False
def start(self):
"""启动观察者线程(TUI 启动时调用)"""
if not self._started:
self.observer.start()
self._started = True
def stop(self):
"""停止观察者线程(TUI 退出时调用)"""
if self._started:
self.observer.stop()
self.observer.join() # 等待线程结束
self._started = False
def watch_namespace(self, namespace_path: str):
"""切换监听目标到指定 Namespace"""
# 1. 取消之前的监听
if self._current_path:
self.observer.unschedule_all()
# 2. 调度新的监听目标
self._current_path = namespace_path
self.observer.schedule(
self,
namespace_path,
recursive=True # 监听 Namespace 下所有子目录
)
# 3. 确保观察者已启动
self.start()
syn media 命令
# 从剪贴板上传图片
syn media paste
# → 自动上传到配置的存储 → 返回 Markdown 链接
# 上传指定文件
syn media upload /path/to/image.png
# 输出示例

媒体存储根目录/
├── 10.001.0015/ # 资源 ID 为目录名
│ ├── screenshot-001.png
│ └── diagram.svg
├── 10.002.0003/
│ └── meeting-photo.jpg
└── _orphan/ # 未关联资源的媒体(待清理)
└── temp-upload.png
┌─────────────────────────────────────────────────────────┐
│ ⚠️ Refile 需要干净的工作区 │
├─────────────────────────────────────────────────────────┤
│ 检测到以下未提交的修改: │
│ M 10/001/10.001.0005-note-学习笔记.md │
│ M 20/001/20.001.0003-task-开发任务.md │
│ │
│ 请先提交或暂存这些修改: │
│ [C] 快速提交 (commit -m 'WIP') │
│ [S] 暂存 (git stash) │
│ [Q] 取消 Refile │
└─────────────────────────────────────────────────────────┘
[[10.001.0001]] | Ripgrep | 字符串替换 |related: ["10.001.0001"] | SQLite 索引 | 解析 YAML,更新数组 |
---
related: ["10.001.0001"] # ← YAML 数组,精确更新 ✓
---
# 设计说明
参考 [[10.001.0001]] 的架构思路... # ← Wiki-style 链接,更新 ✓
任务 10.001.0001 定义了核心接口... # ← 裸 ID,不更新 ✗(可能是历史描述)
--dry-run 预览后手动处理related 是 YAML 数组格式,直接全文替换可能破坏 YAML 结构:
# 原始
related: ["10.001.0001", "10.002.0003"]
# Ripgrep 替换后可能出现的问题(如果替换逻辑不够精确)
related: ["20.001.0005", "10.002.0003"] # ✓ 正确
related: ["20.001.0005", "20.001.0005"] # ✗ 如果 10.002.0003 也被误改
related 字段--dry-run 预览变更:
syn inbox move 00.000.0001 --to work-project --dry-run
# 输出预览:
Will refile: 00.000.0001 → 20.001.0003
Source file:
R inbox/00.000.0001-unprocessed-xxx.md → work-project/20/001/20.001.0003-task-xxx.md
- Frontmatter: jd_number: "00.000.0001" → "20.001.0003"
References to update (2 files):
M personal/10/001/10.001.0005-note-设计笔记.md
- Line 5: related: ["00.000.0001"] → ["20.001.0003"]
- Line 12: [[00.000.0001]] → [[20.001.0003]]
M personal/10/002/10.002.0001-task-开发任务.md
- Line 15: [[00.000.0001]] → [[20.001.0003]]
Skipped (bare IDs not updated):
- personal/10/003/10.003.0001-note-历史记录.md
- Line 8: "任务 00.000.0001 已于 2023 年完成"(裸 ID,需手动处理)
Proceed? [y/N]
syn review daily | 今日到期、今日会议、昨日未完成 |syn review weekly | 本周完成、下周计划、项目进度 |syn inbox process | 处理未分类项目 |
---
jd_number: "10.001.0015"
type: task
namespace: work-project
title: "完成项目设计"
status: in-progress
priority: high
due: 2024-01-15
created: 2024-01-01T10:00:00Z
modified: 2024-01-05T14:30:00Z
tags: [design, urgent]
related: ["10.001.0012", "10.002.0003"]
---
.md 文件.gitignore 配置忽略常见二进制格式syn 启动 | git pull --rebase |git add + git commit |syn 退出 / syn sync | git push |
# .gitattributes
*.md merge=synapse-frontmatter
# .git/config 或全局配置
[merge "synapse-frontmatter"]
name = Synapse Frontmatter Merge
driver = syn merge-driver %O %A %B %Pdef refile_with_link_update(old_id, new_id, old_path, new_path, workspace):
# 0. 前置检查:工作区必须干净
if not is_working_tree_clean():
raise RefileError(
"Refile requires a clean working tree.\n"
"Please commit or stash your changes first:\n"
" syn commit -m 'WIP' 或 git stash"
)
try:
# 1. 移动源文件(暂存)
git_mv(old_path, new_path)
# 1b. 更新源文件的 Frontmatter(jd_number 字段)
source_content = read_file(new_path) # 文件已移动,从新路径读取
source_fm, source_body = parse_frontmatter(source_content)
source_fm['jd_number'] = new_id # 更新 ID
source_fm['modified'] = datetime.now().isoformat() # 更新修改时间
write_file(new_path, serialize_frontmatter(source_fm) + source_body)
git_add(new_path) # ⚠️ 必须暂存源文件的内容修改(git_mv 只暂存重命名)
# 2. 查找并更新所有引用(其他文件)
# ⚠️ 顺序重要:先 YAML 精确处理,再全文替换,避免竞态
modified_files = set()
files_with_related = set() # 记录有 related 字段的文件
# 2a. 先处理 Frontmatter 中的 related 字段(YAML 精确解析)
# 必须先于全文替换,否则 old_id 已被替换,条件判断会失败
related_refs = db_query(
"SELECT file_path FROM resources WHERE related LIKE ?",
(f'%"{old_id}"%',) # 在 JSON 数组字符串中搜索
)
for (file_path,) in related_refs:
content = read_file(file_path)
frontmatter, body = parse_frontmatter(content)
# 安全检查:确保 related 是非空列表
related = frontmatter.get('related')
if isinstance(related, list) and old_id in related:
# 精确更新 YAML 数组
frontmatter['related'] = [
new_id if rid == old_id else rid
for rid in related
]
# 只替换 wiki-style 链接(避免误改非引用上下文)
body = body.replace(f"[[{old_id}]]", f"[[{new_id}]]")
content = serialize_frontmatter(frontmatter) + body
write_file(file_path, content)
modified_files.add(file_path)
files_with_related.add(file_path)
# 2b. 搜索 wiki-style 链接:[[old_id]]
# ⚠️ 只更新明确的引用语法,不盲目替换正文中的 ID 字符串
wiki_refs = rg_search(f"\\[\\[{old_id}\\]\\]", workspace)
for file_path in wiki_refs:
if file_path in files_with_related:
continue # 已被 2a 处理,跳过
content = read_file(file_path)
content = content.replace(f"[[{old_id}]]", f"[[{new_id}]]")
write_file(file_path, content)
modified_files.add(file_path)
# 3. 暂存所有修改
for f in modified_files:
git_add(f)
# 4. 提交事务(+1 是源文件本身)
git_commit(f"refactor: refile {old_id} → {new_id}, update {len(modified_files) + 1} files")
except Exception as e:
# 4. 安全回滚(因为工作区干净,此时可以安全 reset)
git_reset_hard()
logger.error(f"Refile failed, rolled back: {e}")
raise RefileError("操作失败,已回滚所有修改")
def is_working_tree_clean() -> bool:
"""检查工作区是否干净"""
result = subprocess.run(
["git", "status", "--porcelain"],
capture_output=True, text=True
)
return len(result.stdout.strip()) == 0
def smart_rollback(modified_files: list, old_path: str, new_path: str):
"""精确回滚,不影响其他未提交修改"""
# 1. Unstage 暂存的文件
for f in modified_files:
subprocess.run(["git", "reset", "HEAD", f])
# 2. 还原被修改的文件内容
for f in modified_files:
subprocess.run(["git", "checkout", "--", f])
# 3. 移回源文件
if os.path.exists(new_path):
subprocess.run(["git", "mv", new_path, old_path])┌─────────────────────────────────────────────┐
│ Refile: 00.000.0001 - 快速记录 │
├─────────────────────────────────────────────┤
│ [1] personal │
│ [2] work-project │
│ [3] study │
│ [q] Cancel │
└─────────────────────────────────────────────┘
↓ 按 2
┌─────────────────────────────────────────────┐
│ Refile to: work-project │
├─────────────────────────────────────────────┤
│ [20] 项目管理 │
│ [21] 开发 │
│ [22] 设计 │
│ [+] 新建 Area │
│ [q] Back │
└─────────────────────────────────────────────┘
↓ 按 20
┌─────────────────────────────────────────────┐
│ Refile to: work-project/20 │
├─────────────────────────────────────────────┤
│ [001] 任务 │
│ [002] 会议 │
│ [+] 新建 Category │
│ [q] Back │
└─────────────────────────────────────────────┘
↓ 按 001
✓ Moved to: work-project/20/001/20.001.0003-task-快速记录.md
/ 搜索过滤 Namespace/Area/Category
场景:
1. Inbox 中有 00.000.0001(想法A)
2. 笔记 10.002.0005 中写道:"参考 [[00.000.0001]]"
3. Refile 后,A 变为 20.001.0003
4. 结果:[[00.000.0001]] 成为 Dead Link
syn inbox move 且 ID 发生变化时,更新**两种引用形式**:
1. 搜索旧 ID 的引用(使用不同策略):
- Wiki-style 链接:Ripgrep 搜索 [[00.000.0001]]
- Frontmatter related:SQLite 索引查询(精确匹配,避免误改)
2. 自动替换为新 ID
3. 将所有修改一并加入 Git Commit
00unprocessedsyn inbox move <id> --to <ns> --type <type> |syn inbox convert <id> --type task |syn inbox archive <id> |syn inbox delete <id> |status 字段为 archived--include-archived 显示syn inbox move --to archive-namespaceunprocessed 状态不可变,归档时自动转换为 note 类型并标记 status=archived
syn inbox archive 00.000.0001
# unprocessed → note (status=archived)
# 文件重命名:00.000.0001-unprocessed-xxx.md → 00.000.0001-note-xxx.md
┌─────────────┐
│ Inbox 项目 │
└──────┬──────┘
│
┌──────▼──────┐
│ 这是什么? │
│ 需要行动吗? │
└──────┬──────┘
│
┌────────────┼────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ 不需要 │ │ 需要行动 │ │ 参考资料 │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
┌────▼────┐ │ ┌────▼────┐
│ 删除 │ │ │ 归类为 │
│ 或归档 │ │ │ note │
└─────────┘ │ └─────────┘
│
┌────────────┼────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ < 2分钟 │ │ 委派他人 │ │ 需要规划 │
│ 立即做! │ │ │ │ │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ 完成后 │ │ 创建任务 │ │ 创建任务 │
│ 归档/删除│ │ 分配给 │ │ 设置 │
└─────────┘ │ 他人 │ │ 截止日期 │
└─────────┘ └─────────┘
# 批量处理 Inbox
syn inbox process
# 交互式处理(逐项显示,提供操作选项)
# [m]ove | [c]onvert | [a]rchive | [d]elete | [s]kip
# 快速转换为任务
syn inbox convert <id> --type task --due tomorrow --priority high
# 快速归类
syn inbox move <id> --to work-project --type meeting
Enter | 查看详情 | 与全局快捷键一致 |e | 编辑后处理 | 打开编辑器,关闭后弹出处理选项 |m | 移动(Refile) | 层级菜单选择目标位置 |t | 快速转换为 task | 一键转换,保留原位置 |a | 归档 | 标记 status=archived |d | 删除 | 永久删除(Git 可恢复) |m (move) 是最高频操作,TUI 实现层级菜单式 Refile:
### 4.5 上下文感知交互
| 视图 | `Enter` | `Space` | `e` |
|------|---------|---------|-----|
| Task 列表 | 查看详情 | 切换 status: `todo` → `in-progress` → `done` | 编辑 |
| Meeting 列表 | 查看详情 | 切换 status: `scheduled` ↔ `completed` | 编辑 |
| Email 列表 | 查看详情 | 切换 status: `unread` → `read` → `replied` | 编辑 |
| Note 列表 | 查看详情 | (无活跃状态循环,使用 `a` 键归档) | 编辑 |
| Calendar (日) | 资源详情 | 同上(根据资源类型) | 新建事项 |
| Inbox | 查看详情 | (unprocessed 状态不可切换) | 编辑后处理 |
**Space 状态循环**(仅限活跃状态):
- Task: `todo` → `in-progress` → `done` → `todo`
- Meeting: `scheduled` ↔ `completed`(二元切换)
- Email: `unread` → `read` → `replied` → `unread`
- Note: **无 Space 循环**(仅有 `active`/`archived` 两态,使用 `a` 键切换)
**终态处理**(`archived` / `cancelled`):
- `archived` 和 `cancelled` 是**终态**,不参与 Space 循环
- 当资源处于终态时,Space 键**无响应**(或显示提示"资源已归档/取消")
- 进入/退出终态:使用 `a` 键(归档/取消归档)或编辑 Frontmatter
| 状态 | 类型 | 进入方式 | 退出方式 |
|------|------|----------|----------|
| `archived` | task, email | `a` 键归档 | `a` 键取消归档(恢复到 `todo`/`unread`) |
| `archived` | note | `a` 键归档 | `a` 键取消归档(恢复到 `active`) |
| `cancelled` | meeting | 编辑 status 字段 | 编辑 status 字段 |
**`a` 键行为**:
- 活跃状态 → `archived`(归档)
- `archived` → 恢复到默认活跃状态(task→`todo`, email→`unread`, note→`active`)
### 4.6 编辑器配置
| 平台 | 默认 | 可选 |
|------|------|------|
| Windows | notepad | typora, vscode |
| Linux | vim | typora |
| macOS | vim | typora |
### 4.7 日历视图
**时间来源**:
- task: `due` / `start_date`
- meeting: `datetime` / `duration`
**条带说明**:每个条带代表一个资源,高度表示时间跨度,颜色表示类型/优先级。
**交互**:
- `↑↓` 在同一天的条带间切换选中
- `←→` 在日期间导航
- 选中条带时,底部显示资源摘要
- `Enter` 查看资源详情
**跨天显示**:start_date 到 due 的任务、跨天会议,在每天都显示连续条带。
### 4.8 时间线视图
**时间来源**:Git commit 时间戳(资源的 `modified` 时间)
**条带说明**:每列代表一天,条带高度表示当天活动密度,每个条带对应一次 commit。
**交互**:
- `←→` 在时间轴上导航
- `↑↓` 在同一天的 commit 间切换
- 选中 commit 时,底部显示 commit 信息和涉及的资源
- `Enter` 跳转到该资源
### 4.9 会话持久化(Session Persistence)
系统在 `.synapse/session.json`(不入 Git)中记录当前上下文状态:
```json
{
"last_active_namespace": "work-project",
"namespaces": {
"personal": {
"last_view": "calendar",
"selected_date": "2024-01-20",
"recent_files": ["10.001.0001", "10.002.0005"]
},
"work-project": {
"last_view": "list",
"filter_state": {"status": "todo", "tag": "urgent"},
"recent_files": ["20.001.0003"]
}
},
"global_history": [
"syn show 10.001.0001",
"syn search kubernetes"
]
}
syn 时,自动恢复到上次退出的 Namespace 和 ViewCtrl+Tab 可在最近打开的两个文件/视图间快速切换