AGENT.md 是导航,不是真实来源;与代码冲突时以代码为准。 行号、数量、签名都会变;不要把这里的字面量复制进 PR。每条声明都给了核对命令,自己跑一遍再用。 所有方案,必须是行业规范,不能走偏门,歪门邪道必须跟用户沟通后。
这些是反复踩出来的硬约束。不是建议。
不带 sessionId 后缀的全局事件会在多 tab 共存时互相串话。新事件必须遵守这个三段式。
rg 'emit\(' src-tauri/src # 看现有事件全是这个形态GUI 内嵌终端跑 rssh-cli 时,CLI 通过 OSC 7337 转义序列与 GUI 通信,xterm parser 解码后调 store。要扩通信,加个 OSC kind,不要起 socket / pipe / tauri event。
rg 'OSC_RSSH_ID|7337' src src-tauri/src/bincommands/*.rs 写函数 + src-tauri/src/lib.rs 的 generate_handler! 宏注册。漏一处 = 前端 "command not found"。
rg 'generate_handler!' src-tauri/src/lib.rs.pane 是 position: absolute; inset: 0; flex column。任何直接放进 .pane 的根 <div> 必须:
flex: 1;
overflow-y: auto;
min-height: 0; /* 缺这条 flex 子元素不收缩,overflow 失效 */漏了 → 内容溢出整块被裁,没法滚动。HomeScreen 是范例。
走 SecretStore (src-tauri/src/secret/)。Keyring 在 macOS/Windows/Linux 用原生 backend,Android 自动降级到 DB(仍然是这一个抽象,不要绕开)。
rg 'secret_store|SecretStore' src-tauri/srcIMPLEMENTATION_PLAN.md 用完即删(CLAUDE.md 已规定)。不要写新的 NOTES.md / ARCHITECTURE.md / DESIGN.md。导航就这一份。
$state / $derived / $effect / $props,事件 onclick={fn}。看到 $: / export let / on:click ——拒绝合并,让作者升级。
私有 let _x = $state(...) + 导出 getter 函数。不要导出裸 $state 对象,不要在组件里建跨页全局状态。
rg 'export function .* { return _' src/lib/stores/app.svelte.tsRust 端 #[cfg(target_os = "android")],前端 app.isMobile(UA 嗅探,顶层 const)。
rg 'cfg\(target_os|isMobile' src src-tauri/src每个新 feature / UI 改动,PR 描述里至少写清三端各自怎么处理:
- 桌面 GUI:默认目标,必须可用
- 移动 GUI:
app.isMobile路径。没右键、没快捷键、没多窗口、屏幕窄。要么适配(MobileKeybar加按钮、长按代替右键),要么显式声明"移动端不提供" - CLI:
src-tauri/src/bin/rssh.rs。CRUD 类操作大概率要补;纯 UI/可视化类可声明 N/A
允许的结论是 "三端都做" 或 "只在 X 端,因为 Y"。不允许的是没想过——上线后才发现移动端按钮够不着、CLI 改了 schema 但读不出新字段。
rg 'isMobile' src/lib/components # 看现有移动端分支怎么写
rg '#\[cfg\(' src-tauri/src/commands # 看 command 层平台分支| 概念 | 在哪 | 怎么验证 |
|---|---|---|
GUI 二进制 rssh |
src-tauri/src/main.rs |
cat src-tauri/Cargo.toml 看 [[bin]] |
CLI 二进制 rssh-cli |
src-tauri/src/bin/rssh.rs,gated by feature cli |
同上 |
共享 lib rssh_lib |
src-tauri/src/lib.rs,[lib] name="rssh_lib" |
CLI 的 use rssh_lib::*; |
| 概念 | 在哪 | 怎么验证 |
|---|---|---|
| 全局 store | src/lib/stores/app.svelte.ts |
单文件,搜 export function |
| Tab 渲染分发 | src/lib/components/AppShell.svelte |
搜 tab.type === |
| 终端层 | src/lib/components/TerminalPane.svelte |
单文件,xterm + 高亮 + auth 全在内 |
| OSC 解码 | src/lib/osc/handler.ts |
registerRsshOscHandlers |
| 键盘注册表 | src/lib/keyboard/registry.ts |
attachShortcuts |
| i18n | src/lib/i18n/index.svelte.ts + locales/{en,zh}.ts |
t('key') |
| 设计令牌 | src/styles/global.css |
--bg --accent --raised --pressed |
| 概念 | 在哪 | 怎么验证 |
|---|---|---|
| Tauri command 模块 | src-tauri/src/commands/*.rs |
ls src-tauri/src/commands |
| Command 注册总表 | src-tauri/src/lib.rs 的 generate_handler! |
见 R3 |
| 全局运行态 | src-tauri/src/state.rs AppState |
rg 'AppState' src-tauri/src |
| SSH 客户端 | src-tauri/src/ssh/client.rs |
russh wrapper |
| SFTP | src-tauri/src/ssh/sftp.rs |
russh-sftp wrapper |
| 端口转发 | src-tauri/src/ssh/forward.rs |
local/remote/dynamic |
| PTY(仅桌面) | src-tauri/src/terminal/pty.rs,#[cfg(not(target_os="android"))] |
portable-pty |
| 数据库 | src-tauri/src/db/,rusqlite bundled |
看 db/schema.rs |
| 密钥抽象 | src-tauri/src/secret/,trait + keyring/db 两实现 |
见 R5 |
| 错误类型 | src-tauri/src/error.rs AppError |
thiserror 派生 |
| GitHub 同步 | src-tauri/src/sync/github.rs |
commands/sync.rs 入口 |
| Tab type | 格式 | 出处 |
|---|---|---|
home |
字面量 "home",唯一固定,不可关闭 |
app.svelte.ts 初始 _tabs |
ssh / local / edit |
"<type>:<uuid>" |
crypto.randomUUID() 调用点 |
forward |
"fwd:<forward_id>:<timestamp>" |
HomeScreen / osc/handler.ts |
rg 'crypto\.randomUUID' src/lib # 看 tab id 怎么造npm run build # Vite 编前端
cd src-tauri && cargo check # Rust 类型检查
./build-mac.sh # macOS aarch64 .dmg
./build-android.sh # APK + AAB(需 ANDROID_HOME/NDK)
npm run tauri dev # 本地跑无 lint,无 unit test。验证靠编译 + 手动点。
启动时若不是克隆窗口,调 reconcile_sessions(activeIds=[]) 让后端清孤儿。克隆窗口必须跳过——它们与父窗共享 AppState.sessions,传空列表会把别窗口的 session 全杀。判定靠 window.__rssh_clone 标志,由 open_tab_in_new_window 注入。
rg '__rssh_clone|reconcile_sessions' src src-tauri/src/usr/local/bin/rssh(CLI)会 shadow /usr/bin/rssh(GUI)。CLI 在无 subcommand + 有 DISPLAY/WAYLAND_DISPLAY 时主动 fork GUI 二进制。改 CLI 启动逻辑必须保留 canonicalize 自循环检测。
rg 'try_launch_gui|RSSH_APP' src-tauri/src/binTerminalPane.svelte 把用户配的 keyword regex 插入 ANSI 24-bit 转义到 stdin 流。必须保留已有 ANSI 序列——别用朴素 replace。改这块前先理解现有 lexer 状态机。
AppState.auth_waiters: HashMap<tab_id, oneshot::Sender<Vec<String>>>。事件 ssh:auth_prompt:{tabId} → 前端模态 → ssh_auth_respond command → channel send。Tab 中途关闭要清 waiter,否则 leak。
改表结构 / 改 SecretStore key 命名时,同时审 CLI 路径。CLI 不会自动跟随 command 层的逻辑变更。
rg 'db::|secret_store' src-tauri/src/bin/rssh.rsCredential 上的开关,config push / commands/sync.rs 据此过滤。改同步逻辑必须看两处。
UA 嗅探,启动一次。需要响应式断点 → 自己加 $state + resize 监听,别误以为现成。
前端 invoke("name") 字符串硬编码。Rust 端改函数名 = 前端运行时炸。要改 grep 全局 invoke(" 同步。
rg 'invoke\("' src/lib- Rust:snake_case 函数与字段,PascalCase 类型
- TypeScript:camelCase 函数与变量,PascalCase 类型与组件
- State getter:动词短语
tabs()activeTab()settingsActive(),不带get前缀 - 错误消息:中文,面向用户
- Rust:
AppErrorenum +AppResult<T>,命令返回它,自动序列化成字符串 - 前端:
try { await invoke(...) } catch (e) { app.toast(...) },无全局 boundary - 不要静默吞错(
.catch(() => {}))除非确认是清理路径
用 src/styles/global.css 的变量与现有 .neu-* / .btn* 类。不要自己挑十六进制色——主题切换会破。
- 渐进式 > 大爆炸
- 一个 PR 一件事,不要顺手重构无关代码
- UI 改动跑
npm run tauri dev实际点一遍,类型通过 ≠ 功能正确
npm run build通过cd src-tauri && cargo check通过- 改了 command?检查
lib.rs的generate_handler!(R3) - 改了事件名?前后端同步 grep
<domain>:(R1) - 改了 schema?审
db/schema.rsmigration + CLI 路径(P5) - 改了 UI?跑 dev 点过
- 创建分析/计划文档(R6)
- 起新 IPC 通道(R3 / R2)
- 在组件里建跨页全局状态(R8)
- 把 secret 写 DB 明文(R5)
- 用
--no-verify跳 hook - 给 tab 根容器忘记
flex:1; overflow-y:auto; min-height:0(R4) - 复制本文行号字面量进代码或 PR 描述(开头免责声明)
- 只为单一平台写功能而没声明其他两端的处理(R10)