feat: 支持 reasonix:// deep link 从网页打开桌面端对话 - #7944
Conversation
There was a problem hiding this comment.
💡 Codex Review
DeepSeek-Reasonix/internal/tool/builtin/bash.go
Lines 684 to 685 in d524862
When the bash tool runs on Windows, this early return occurs before the new REASONIX_THREAD_ID injection. Consequently attribution-aware commands launched from Windows sessions still fall back to the synthetic marker, defeating the change precisely on that supported desktop platform; only the Unix-specific PATH merge should be skipped.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "copyright": "Copyright © 2026 Reasonix Contributors", | ||
| "comments": "Reasonix desktop — a Wails shell around the Go kernel." | ||
| "comments": "Reasonix desktop — a Wails shell around the Go kernel.", | ||
| "protocols": [ |
There was a problem hiding this comment.
Declare protocols at the Wails config root
Wails v2 expects protocols as a top-level sibling of info, not as a member of the product-info object. In this location the protocol declaration is not consumed when generating the macOS bundle or installer metadata, so the OS never registers reasonix:// and none of the new handlers can be reached.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
不修改 — 附证据说明这是误判。
Wails v2.12.0 的 Info 结构体包含 Protocols 字段:
// internal/project/project.go:232
type Info struct {
...
FileAssociations []FileAssociation `json:"fileAssociations"`
Protocols []Protocol `json:"protocols"`
}macOS bundle 的 Info.plist 模板直接消费它(pkg/buildassets/build/darwin/Info.plist):
{{if .Info.Protocols}}
<key>CFBundleURLTypes</key>
...
{{end}}
Windows installer 的 NSIS 脚本同样消费(wails_tools.nsh 的 wails.associateCustomProtocols 遍历 .Info.Protocols)。
所以 info.protocols 是 Wails v2 的正确位置,protocols 作为顶层兄弟反而不会被解析。本 PR 的 wails.json 无需改动。
| a.emitRuntimeEvent("app:open-topic", map[string]string{ | ||
| "scope": scope, | ||
| "workspaceRoot": workspace, | ||
| "topicID": topicID, | ||
| }) |
There was a problem hiding this comment.
Queue cold-start links until the frontend subscribes
On a cold launch where tab restoration completes before the React app:open-topic effect is mounted, this runtime event is emitted with no listener and is discarded; tabsRestored only establishes backend readiness, not frontend readiness. The primary cold-start reasonix://threads/... flow therefore intermittently just opens the window without navigating, so the URL needs to remain pending until a frontend-ready handshake or an explicit frontend fetch.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
已修复 ✅ — 冷启动 pending 队列已实现。
后端新增 frontendReady atomic.Bool + pendingDeepLinks []string 队列:
handleDeepLink在 frontendReady 未置位时把 URL 入队(queueDeepLink),不再立即 emit 丢失- 前端挂载
app:open-topic/app:new-topic监听后调用MarkFrontendReady()(置位 + 触发 flush),再DrainPendingDeepLinks()拉取排队 URL 并逐条处理 - 与 macOS OnUrlOpen 的 tabsRestored 门控互补:tabsRestored 保证后端就绪,frontendReady 保证前端监听器挂载,两层都有才 emit
已加单测 TestQueueAndDrainPendingDeepLinks 覆盖排队与清空语义。
| if workspaceRoot == "" { | ||
| scope = "global" | ||
| } | ||
| meta, err := a.ActivateTopic(scope, workspaceRoot, "", "") |
There was a problem hiding this comment.
Route new-session links through frontend navigation
For reasonix://new, calling ActivateTopic directly mutates and potentially prunes the backend tab set without running the frontend's seedActiveTabMeta/refresh path used by normal navigation. When another conversation is visible, its UI remains selected while the goal starts on the newly created backend tab (the non-active agent:ready event is ignored), usually until the 15-second metadata poll, so this path should notify or execute through the frontend before submitting.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
已修复 ✅ — reasonix://new 改为走前端导航路径。
后端 handleDeepLink 的 new 分支不再直接调 ActivateTopic/SubmitInitialGoalToTab,改为 emit app:new-topic 事件(携带 scope/workspaceRoot/goal)。前端监听后走 openBlankSession(enqueueNavigation blank 分支,会 seedActiveTabMeta + refreshLatestTabMetas)→ 轮询新 tab ready → commitThenSend 提交 goal。与正常 UI 新建会话的导航路径完全一致,不再出现"另一会话仍选中"的问题。
| OnUrlOpen: func(rawURL string) { | ||
| go func() { | ||
| <-app.tabsRestoredSignal() | ||
| app.handleDeepLink(rawURL) | ||
| }() | ||
| }, |
There was a problem hiding this comment.
Handle protocol launches outside macOS
The only path that forwards a protocol URL to handleDeepLink is this macOS callback. On Windows, initial-launch URLs are ignored by parseDesktopLaunchArgs and second-instance URLs are discarded because singleInstanceLock ignores SecondInstanceData.Args; the packaged Linux desktop entry likewise has neither a %u argument nor an x-scheme-handler MIME type. Thus reasonix:// cannot open a conversation on either supported non-macOS desktop platform even after the protocol declaration is corrected.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
已修复 ✅ — Windows 首次启动 + 二次点击均已支持。
parseDesktopLaunchArgs新增DeepLinkURL字段:识别reasonix://前缀的 argv(Windows installer 注册的 scheme 以"%1"传入)- 非 macOS 首次启动:main.go 把 argv 里的 URL 入队到
queueDeepLink(复用 frontend-ready drain 机制,冷启动语义与 macOS 一致) - Windows 二次点击:
OnSecondInstanceLaunch转发SecondInstanceData.Args中的reasonix://URL 到queueDeepLink - macOS 仍走
OnUrlOpen(Apple Events),不重复处理
已加单测 TestParseDesktopLaunchArgsDeepLink 覆盖 argv 解析(含大小写 scheme)。已用 GOOS=windows 交叉编译验证通过。
Linux desktop entry 的 %u / x-scheme-handler 注册属于打包配置,不在本 PR 代码范围(wails 的 Linux 打包不自动生成该 MIME 声明),可作为后续独立 PR。
| if workspace == "" { | ||
| workspace = target.workspaceRoot | ||
| } |
There was a problem hiding this comment.
Trust the resolved session workspace over the query
When a session filename resolves successfully but the URL also contains a stale or incorrect workspace value, this conditional preserves the query value instead of the workspace recorded by the matching session. The frontend then searches the wrong project for the resolved topic and may create a new empty session with that topic ID rather than opening the matched conversation; a successful filename resolution should make the session metadata authoritative.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
已修复 ✅ — 解析成功后 session 元数据权威覆盖。
handleDeepLink threads 分支改为无条件 workspace = target.workspaceRoot(不再仅在空值时覆盖),并加注释说明:URL 的 workspace 参数可能过期或缺失,不一致会导致前端搜错项目并可能为解析出的 topic 新建空会话。
| return | ||
| } | ||
| if strings.TrimSpace(goal) != "" { | ||
| if _, err := a.SubmitInitialGoalToTab(meta.ID, goal, goal, goal, nil, "", ""); err != nil { |
There was a problem hiding this comment.
Preserve the configured approval mode for deep-link sessions
ActivateTopic creates the new tab with the user's configured desktop tool-approval default, but passing an empty final argument here is normalized to ask by SubmitInitialGoalToTab and overwrites that profile. Users configured for auto or yolo therefore get different approval behavior only for sessions created through reasonix://new; pass the tab's actual approval mode instead of an empty value.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
已修复 ✅ — 保留用户配置的 approval mode。
由于 new 分支已改为前端导航(见 P2-4 回复),goal 提交走 commitThenSend → sendToTab,其中 initialGoal.toolApprovalMode 取自新建 tab 元数据的 tab.toolApprovalMode(来自 desktopNewSessionDefaults,即用户配置的 DesktopDefaultToolApprovalMode)。不再传空字符串被归一化为 ask。
| path := filepath.Join(dir, "deeplink.log") | ||
| f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) |
There was a problem hiding this comment.
Avoid persisting raw deep-link prompts in a public log
Every incoming URL is written verbatim, so reasonix://new?...&prompt=... permanently duplicates potentially sensitive prompt text and workspace paths in deeplink.log; the file is also created with mode 0644 and has no rotation or size bound. Redact query values and use private permissions or the existing structured logging path rather than retaining the raw URL indefinitely.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
已修复 ✅ — 日志脱敏 + 私有权限。
logDeepLink的所有值经redactDeepLinkValue过滤:含://的 URL 值剥离 query(prompt 文本、workspace 路径不再落盘),保留 scheme/host 便于诊断- 文件权限从 0644 改为 0600(仅当前用户可读)
- 非 URL 诊断值(topic、scope、sessionPath)原样保留可读
已加单测 TestRedactDeepLinkValue 覆盖:URL query 被剥离、非 URL 值不变。
| if _, err := a.SubmitInitialGoalToTab(meta.ID, goal, goal, goal, nil, "", ""); err != nil { | ||
| slog.Warn("deeplink: submit goal failed", "tab", meta.ID, "err", err) | ||
| } |
There was a problem hiding this comment.
Wait for the new controller before submitting the prompt
When ActivateTopic has to create a new tab, it starts the controller build asynchronously and returns before the runtime reaches the ready phase. This immediate SubmitInitialGoalToTab therefore fails workspaceRuntimeAdmissionErr with “workspace is still starting,” is only logged, and is never retried, so most reasonix://new?...&prompt=... links open an empty conversation without submitting their prompt.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
已修复 ✅ — 前端轮询等待新 tab controller ready。
app:new-topic 前端处理:openBlankSession 后轮询 refreshTabMetas(500ms × 最多 30 次),直到新 tab ready === true && runtime.phase === "ready" && !startupErr 才 commitThenSend 提交 goal;若出现 startupErr 则放弃(不无限重试)。与现有 commitThenSend 的 ready 检查一致,不再因 workspaceRuntimeAdmissionErr 静默丢失 prompt。
| a.emitRuntimeEvent("app:open-topic", map[string]string{ | ||
| "scope": scope, | ||
| "workspaceRoot": workspace, | ||
| "topicID": topicID, | ||
| }) |
There was a problem hiding this comment.
Preserve the named session path through navigation
A filename deep link resolves an exact match.path, but the result and emitted payload retain only the topic, scope, and workspace. Because the frontend then calls handleOpenTopic with an empty session path, topics with multiple saved branches open whichever session findTopicSessionForTarget considers latest rather than the session explicitly named in the URL; include the resolved path in the event and pass it through the existing sessionPath argument.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
已修复 ✅ — resolvedSessionTarget 增加 sessionPath 字段并全程透传。
resolveSessionFile命中时记录match.path(解析出的确切 session 文件)handleDeepLinkemitapp:open-topic携带sessionPath- 前端监听器把
sessionPath传给handleOpenTopic(第 4 参数,enqueueNavigation topic 分支本来就有该字段),openTopicTarget在非 single-surface 布局下走openTopicSession(scope, workspaceRoot, topicId, sessionPath),打开 URL 指定的那条分支而非 findTopicSessionForTarget 的最新会话
| // even when the caller only supplied a session name. | ||
| scope := "project" | ||
| resolved := false | ||
| if target, ok := a.resolveSessionFile(topicID); ok { |
There was a problem hiding this comment.
Search the supplied workspace for session filenames
Session filename resolution scans only knownSessionDirs, which is built from the global store, registered projects, and currently open tabs; it never considers the workspace query parameter. A valid link to a CLI-created session in a workspace that has not yet been registered in this desktop instance therefore misses resolution and is rejected as a non-topic_ phantom even though the URL provides the exact project root; add that workspace's session directory to the lookup.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
已修复 ✅ — resolveSessionFile 接受 workspace hint 并加入查找范围。
resolveSessionFile(name, workspaceHint)在knownSessionDirs()之外追加desktopSessionDir(workspaceHint)(URL 的 workspace 参数指定的项目目录),CLI 创建的未注册 workspace 会话也能解析handleDeepLink调用时传入 URL 的 workspace query- 已加说明注释:URL 提供确切项目根时,即便该 workspace 尚未在桌面实例注册,文件名解析也能命中
d524862 to
72a2440
Compare
Add a reasonix:// URL scheme so a web-hosted taskboard can open the matching desktop conversation: - wails.json declares the reasonix scheme (Info.plist CFBundleURLTypes) - main.go wires Mac.OnUrlOpen, gated on tabsRestored for cold starts - app.go handleDeepLink parses reasonix://threads/<id> and resolves session file names or topic ids to the conversation's scope/workspace - App.tsx listens for app:open-topic and runs the same activateTopic path as a sidebar click (refreshes tab header and conversation content) - bash.go injects REASONIX_THREAD_ID so taskctl writes carry the real conversation id instead of a synthetic marker Cards without a real session (heartbeat attribution markers) are skipped gracefully instead of erroring the app.
72a2440 to
cbf0c5c
Compare
变更内容
新增
reasonix://URL scheme,让网页端看板可以唤起桌面端并打开对应对话。功能
reasonix://threads/<id>— 打开已有会话(支持 session 文件名和 topic_id 两种格式)reasonix://new?workspace=<path>&prompt=<text>— 新建会话并提交初始目标改动文件
desktop/wails.jsondesktop/main.godesktop/app.godesktop/frontend/src/App.tsxinternal/tool/builtin/bash.go验证
Cache-impact: none - 未触碰缓存敏感路径(internal/boot、internal/provider)
Cache-guard: 现有 go test ./internal/tool/builtin 覆盖 bashCommandEnv 改动
Documentation-impact: none - URL scheme 是新增能力,不改变现有文档描述的任何 CLI/桌面行为,现有文档保持正确