Skip to content
Open
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: 11 additions & 0 deletions docs/en/12_GUIDED_WORKFLOW_TOUR.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ Common examples:
- experiment summaries
- result reports
- paper drafts
- LaTeX source files and BibTeX references

In practice, many users treat Markdown files in the quest as a private local-first notebook for:

Expand All @@ -340,6 +341,16 @@ In practice, many users treat Markdown files in the quest as a private local-fir
- findings
- team coordination

When you open a LaTeX project folder, the browser editor treats the folder as one LaTeX workspace. Source files such as `main.tex`, chapter files under subfolders, BibTeX files, and style files switch inside the same editor, Overleaf-style, instead of spawning separate top-level editors or internal source tabs. The file picker still lists the full project source tree for quick switching.

The editor auto-saves source edits shortly after you type. Background autosaves only persist the source; they do not start PDF compilation. Manual saves default to compile-on-save: `Ctrl/Cmd+S` or the `Save` button saves the active LaTeX file and then starts one PDF compilation when the save succeeds. `Save & Compile` remains available for an explicit compile action and still saves the current source before starting PDF compilation.

If another process changes the active LaTeX source file while it is open, such as an AI edit or terminal command, the editor refreshes automatically when the local buffer has no unsaved edits. If the local buffer is dirty, autosave pauses and the editor asks you to either reload the external version or explicitly overwrite it, so an ordinary save cannot silently replace external changes.

The `History` button in the LaTeX toolbar opens a Git-backed version view scoped to the current LaTeX folder. While History is open, it takes over the LaTeX content area instead of showing the source editor and PDF preview side by side. You can create named versions, inspect changed source files, select any history item to automatically compare it with the latest archive point, click any changed file to view a red/green unified diff, and restore either the active file or the whole LaTeX project. In addition to manual versions, the editor asks the backend to create Overleaf-style automatic checkpoints after idle saves, AI/file-diff edits, visibility changes, or compile actions. The backend applies a shared policy so checkpoints are not too frequent: it skips very recent checkpoints, creates a new visible version after significant source changes, and also creates one after a long enough editing interval even for smaller edits. Compile actions keep hidden build snapshots for reproducibility; the History panel hides those by default and can show them with the build-snapshot toggle.

After a successful compile, the PDF preview uses SyncTeX metadata when available. Double-click a rendered PDF word to jump back to the matching LaTeX source file and select the corresponding source token; the editor uses the PDF word box plus multiple SyncTeX samples to avoid broad line-level selections. Older builds without SyncTeX data need to be recompiled before PDF-to-source jumps are available.

### 6.5 Canvas

Canvas makes the research map visible.
Expand Down
11 changes: 11 additions & 0 deletions docs/zh/12_GUIDED_WORKFLOW_TOUR.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ Explorer 是 quest 的文件视角。
- 实验总结
- 结果报告
- 论文草稿
- LaTeX 源文件与 BibTeX 引用

很多用户会把 quest 里的 Markdown 文件当作一个本地优先、类似 Notion 的私有笔记本,用来记录:

Expand All @@ -338,6 +339,16 @@ Explorer 是 quest 的文件视角。
- 发现
- 协作信息

打开 LaTeX 项目文件夹时,浏览器编辑器会把该文件夹作为一个统一的 LaTeX 工作区处理。`main.tex`、章节子目录中的 `.tex` 文件、BibTeX 文件和样式文件会像 Overleaf 一样在同一个编辑器里直接切换,而不是为每个文件创建新的顶层编辑器或内部源码标签页。文件选择器仍会列出完整项目源码树,方便快速切换。

编辑器会在输入后短时间内自动保存源文件。后台自动保存只负责落盘源码,不会启动 PDF 编译。手动保存默认开启保存后自动编译:`Ctrl/Cmd+S` 或 `保存` 按钮会先保存当前 LaTeX 文件,保存成功后启动一次 PDF 编译。`保存并编译` 仍可用于显式编译,并会先保存当前源码,再启动 PDF 编译。

如果其它进程在当前 LaTeX 源文件打开期间修改了它,例如 AI 编辑或终端命令,且本地缓冲区没有未保存内容,编辑器会自动刷新到外部版本。若本地缓冲区已被修改,自动保存会暂停,并提示你选择重新载入外部版本或明确覆盖外部版本,避免普通保存静默覆盖外部修改。

LaTeX 工具栏中的 `历史版本` 按钮会打开限定在当前 LaTeX 文件夹内的 Git 版本视图。历史版本打开时会占满 LaTeX 内容区,不再同时显示源码编辑器和 PDF 预览。你可以创建命名版本、查看变更源码文件、点击任意历史版本后自动与最新的存档点比较,点击任意变更文件查看红/绿 unified diff,并恢复当前文件或整个 LaTeX 项目。除手动版本外,编辑器会在空闲自动保存、AI / 文件 diff 修改、页面隐藏或编译动作后请求后端创建类似 Overleaf 的自动检查点。后端统一执行“不要太频繁”的策略:距离上次检查点太近会跳过;源码改动达到一定量时创建新的可见版本;即使改动较小,只要编辑间隔足够长也会创建版本。编译动作仍会为可复现性保留隐藏的构建快照;历史版本面板默认隐藏这些构建快照,可通过“显示构建快照”开关查看。

成功编译后,PDF 预览会在可用时使用 SyncTeX 元数据。双击 PDF 中渲染出的某个单词时,编辑器会结合 PDF 单词框和多点 SyncTeX 采样跳转到匹配的 LaTeX 源文件,并选中对应的源码 token,避免退化成大范围行级选中。没有 SyncTeX 数据的旧构建需要重新编译后才能使用 PDF 到源码跳转。

### 6.5 Canvas

Canvas 会把研究地图直接展示出来。
Expand Down
82 changes: 81 additions & 1 deletion src/deepscientist/daemon/api/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2046,7 +2046,7 @@ def document_save(self, quest_id: str, document_id: str, body: dict) -> dict:
try:
return self.app.quest_service.save_document(
quest_id,
document_id,
unquote(document_id),
body["content"],
previous_revision=body.get("revision"),
)
Expand Down Expand Up @@ -2121,6 +2121,71 @@ def latex_compile(self, project_id: str, folder_id: str, body: dict) -> dict:
auto=body.get("auto"),
)

def latex_manifest(self, project_id: str, folder_id: str) -> dict:
return self.app.latex_service.manifest(project_id, folder_id)

def latex_versions(self, project_id: str, folder_id: str, path: str = "") -> dict:
query = self.parse_query(path)
limit_raw = ((query.get("limit") or ["30"])[0] or "30").strip()
include_hidden = ((query.get("include_hidden") or ["false"])[0] or "").strip().lower() in {"1", "true", "yes", "on"}
try:
limit = int(limit_raw)
except ValueError:
limit = 30
return self.app.latex_service.list_versions(project_id, folder_id, limit=limit, include_hidden=include_hidden)

def latex_version_create(self, project_id: str, folder_id: str, body: dict) -> dict:
return self.app.latex_service.create_version(
project_id,
folder_id,
label=body.get("label"),
description=body.get("description"),
source=body.get("source"),
author=body.get("author"),
build_id=body.get("build_id"),
allow_empty=body.get("allow_empty", True) is not False,
)

def latex_version_auto(self, project_id: str, folder_id: str, body: dict) -> dict:
return self.app.latex_service.maybe_create_auto_version(
project_id,
folder_id,
reason=body.get("reason"),
active_file=body.get("active_file"),
)

def latex_versions_compare(self, project_id: str, folder_id: str, path: str = "") -> dict:
query = self.parse_query(path)
base = ((query.get("base") or [""])[0] or "").strip()
head = ((query.get("head") or [""])[0] or "").strip()
if not base or not head:
return {"ok": False, "message": "`base` and `head` are required."}
return self.app.latex_service.compare_versions(project_id, folder_id, base=base, head=head)

def latex_version(self, project_id: str, folder_id: str, version_id: str) -> dict:
return self.app.latex_service.get_version(project_id, folder_id, version_id)

def latex_version_files(self, project_id: str, folder_id: str, version_id: str) -> dict:
return self.app.latex_service.version_files(project_id, folder_id, version_id)

def latex_version_file(self, project_id: str, folder_id: str, version_id: str, path: str = "") -> dict:
query = self.parse_query(path)
file_path = ((query.get("path") or [""])[0] or "").strip()
if not file_path:
return {"ok": False, "message": "`path` is required."}
return self.app.latex_service.version_file(project_id, folder_id, version_id, file_path)

def latex_version_restore(self, project_id: str, folder_id: str, version_id: str, body: dict) -> dict:
return self.app.latex_service.restore_version(
project_id,
folder_id,
version_id,
mode=body.get("mode"),
path=body.get("path"),
expected_head=body.get("expected_head"),
conflict_policy=body.get("conflict_policy"),
)

def latex_builds(self, project_id: str, folder_id: str, path: str) -> list[dict]:
query = self.parse_query(path)
limit_raw = ((query.get("limit") or ["10"])[0] or "10").strip()
Expand All @@ -2133,6 +2198,21 @@ def latex_builds(self, project_id: str, folder_id: str, path: str) -> list[dict]
def latex_build(self, project_id: str, folder_id: str, build_id: str) -> dict:
return self.app.latex_service.get_build(project_id, folder_id, build_id)

def latex_synctex_edit(self, project_id: str, folder_id: str, build_id: str, body: dict) -> dict:
return self.app.latex_service.synctex_edit(
project_id,
folder_id,
build_id,
page=body.get("page"),
x=body.get("x"),
y=body.get("y"),
pdf_word=body.get("pdf_word"),
pdf_context_words=body.get("pdf_context_words"),
pdf_context_index=body.get("pdf_context_index"),
pdf_word_bbox=body.get("pdf_word_bbox"),
pdf_word_center=body.get("pdf_word_center"),
)

def latex_build_pdf(self, project_id: str, folder_id: str, build_id: str) -> tuple[int, dict, bytes]:
payload, file_name = self.app.latex_service.get_build_pdf(project_id, folder_id, build_id)
headers = {
Expand Down
10 changes: 10 additions & 0 deletions src/deepscientist/daemon/api/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,18 @@
("DELETE", re.compile(r"^/api/v1/annotations/(?P<annotation_id>[^/]+)$"), "annotation_delete"),
("POST", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/init$"), "latex_init"),
("POST", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/compile$"), "latex_compile"),
("GET", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/manifest$"), "latex_manifest"),
("GET", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/versions$"), "latex_versions"),
("POST", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/versions$"), "latex_version_create"),
("POST", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/versions/auto$"), "latex_version_auto"),
("GET", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/versions/compare$"), "latex_versions_compare"),
("GET", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/versions/(?P<version_id>[^/]+)$"), "latex_version"),
("GET", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/versions/(?P<version_id>[^/]+)/files$"), "latex_version_files"),
("GET", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/versions/(?P<version_id>[^/]+)/file$"), "latex_version_file"),
("POST", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/versions/(?P<version_id>[^/]+)/restore$"), "latex_version_restore"),
("GET", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/builds$"), "latex_builds"),
("GET", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/builds/(?P<build_id>[^/]+)$"), "latex_build"),
("POST", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/builds/(?P<build_id>[^/]+)/synctex/edit$"), "latex_synctex_edit"),
("GET", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/builds/(?P<build_id>[^/]+)/pdf$"), "latex_build_pdf"),
("GET", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/builds/(?P<build_id>[^/]+)/log$"), "latex_build_log"),
("GET", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/archive$"), "latex_archive"),
Expand Down
5 changes: 4 additions & 1 deletion src/deepscientist/daemon/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8772,6 +8772,9 @@ def _dispatch(self, method: str) -> None:
"document_asset",
"terminal_restore",
"terminal_history",
"latex_versions",
"latex_versions_compare",
"latex_version_file",
"latex_builds",
"arxiv_list",
"annotations_file",
Expand Down Expand Up @@ -8799,7 +8802,7 @@ def _dispatch(self, method: str) -> None:
"repair_create",
"repair_close",
"hardware_update",
} or route_name in {"document_open", "document_asset_upload", "quest_file_create_folder", "quest_file_upload", "quest_file_rename", "quest_file_move", "quest_file_delete", "chat_upload_create", "chat_upload_delete", "chat", "command", "quest_control", "quest_message_read_now", "quest_message_withdraw", "config_save", "quest_create", "quest_baseline_binding", "run_create", "qq_inbound", "connector_inbound", "docs_open", "bash_stop", "quest_settings", "quest_bindings", "quest_delete", "quest_layout_update", "terminal_session_ensure", "terminal_attach", "terminal_input", "stage_view", "latex_init", "latex_compile", "system_update_action", "weixin_login_qr_start", "weixin_login_qr_wait", "arxiv_import", "annotation_create", "auth_login", "auth_rotate"}:
} or route_name in {"document_open", "document_asset_upload", "quest_file_create_folder", "quest_file_upload", "quest_file_rename", "quest_file_move", "quest_file_delete", "chat_upload_create", "chat_upload_delete", "chat", "command", "quest_control", "quest_message_read_now", "quest_message_withdraw", "config_save", "quest_create", "quest_baseline_binding", "run_create", "qq_inbound", "connector_inbound", "docs_open", "bash_stop", "quest_settings", "quest_bindings", "quest_delete", "quest_layout_update", "terminal_session_ensure", "terminal_attach", "terminal_input", "stage_view", "latex_init", "latex_compile", "latex_version_create", "latex_version_auto", "latex_version_restore", "latex_synctex_edit", "system_update_action", "weixin_login_qr_start", "weixin_login_qr_wait", "arxiv_import", "annotation_create", "auth_login", "auth_rotate"}:
payload = result(**params, body=body)
elif route_name == "config_validate":
payload = result(body)
Expand Down
Loading