Skip to content
Merged
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
16 changes: 0 additions & 16 deletions build/installer/custom-uninstall.nsh

This file was deleted.

75 changes: 75 additions & 0 deletions build/installer/installer.nsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
; preInit: clear legacy InstallLocation cache so the next install falls
; back to the perUser default path. Only clean on fresh installs.
; The whole body is guarded by !ifndef UNINSTALLER_OUT_FILE so it's a
; no-op in the uninstaller build (where $INSTDIR mustn't be touched).
!macro preInit
!ifndef UNINSTALLER_OUT_FILE
${IfNot} ${isUpdated}
DeleteRegValue HKLM "Software\AgentDock" "InstallLocation"
DeleteRegValue HKCU "Software\AgentDock" "InstallLocation"
StrCpy $INSTDIR "$LOCALAPPDATA\Programs\${APP_FILENAME}"
${EndIf}
!endif
!macroend

; customInstall: force the default install path to the perUser
; convention ($LOCALAPPDATA\Programs\AgentDock). Skipped on upgrade
; and no-op in the uninstaller build.
!macro customInstall
!ifndef UNINSTALLER_OUT_FILE
${IfNot} ${isUpdated}
StrCpy $INSTDIR "$LOCALAPPDATA\Programs\${APP_FILENAME}"
${EndIf}
!endif
!macroend

; customUnInstall: runs inside Section "Uninstall". Removes data from
; every location AgentDock may have written to. NOTE: $USERPROFILE is
; NOT a valid NSIS built-in variable — use $PROFILE (= C:\Users\<u>),
; which is reliable across OneDrive/redirected-folder setups.
!macro customUnInstall
; 1. Install dir (Local\Programs\AgentDock)
StrCpy $0 "$LOCALAPPDATA\Programs\${APP_FILENAME}"
${If} ${FileExists} "$0"
RMDir /r "$0"
${EndIf}
; 2. Roaming userData (electron default $APPDATA\AgentDock)
StrCpy $0 "$APPDATA\${APP_FILENAME}"
${If} ${FileExists} "$0"
RMDir /r "$0"
${EndIf}
; 3. PerMachine data (C:\ProgramData\AgentDock)
StrCpy $0 "C:\ProgramData\AgentDock"
${If} ${FileExists} "$0"
RMDir /r "$0"
${EndIf}
; 4. Legacy homedir global DB (pre-v0.3 fallback)
; $PROFILE is an NSIS built-in = C:\Users\<u>. Reliable across
; OneDrive/redirected-folder setups (unlike deriving from $DESKTOP,
; which becomes C:\Users\<u>\OneDrive\Desktop under OneDrive).
StrCpy $0 "$PROFILE\.agentdock"
${If} ${FileExists} "$0"
RMDir /r "$0"
${EndIf}
!macroend

; customUnInstallSection: Section-level fallback for homedir cleanup.
; Runs AFTER customUnInstall, inside Section "Uninstall".
!macro customUnInstallSection
Section "-un.AgentDockLegacyCleanup"
; $PROFILE = C:\Users\<u> (NSIS built-in, reliable in Section context)
StrCpy $0 "$PROFILE\.agentdock"
${If} ${FileExists} "$0"
RMDir /r "$0"
${EndIf}
; Also remove Roaming and Local paths as defense-in-depth
StrCpy $1 "$APPDATA\${APP_FILENAME}"
${If} ${FileExists} "$1"
RMDir /r "$1"
${EndIf}
StrCpy $2 "$LOCALAPPDATA\Programs\${APP_FILENAME}"
${If} ${FileExists} "$2"
RMDir /r "$2"
${EndIf}
SectionEnd
!macroend
137 changes: 137 additions & 0 deletions docs/fix-stale-project-tabs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# 修复经验: 全新安装后顶部残留大量 project tab

## 问题现象

用户全新安装 AgentDock(无论 perUser 还是 perMachine,无论安装/卸载多少次),
启动后顶部出现大量 project tab —— 包括:

- `AgentDock` — 安装目录本身
- `Copilot-Switch`、`AgentDock (1)` — 历史打开过的项目
- `a2b1c2fe-591`、`bed4c452-74d` — git worktree 目录
- `win-unpacked` — build 产物目录
- `agentdock-e2e-*`(多个)— e2e 测试自动生成的 temp 目录

关键:**用户全新安装后没有执行任何操作**,tab 就已经存在。

## 根因(两个独立问题)

### 根因 1: 打包后 `process.cwd()` 被 auto-register 成项目

`electron/main.ts` 启动时执行:

```ts
activeProjectPath = process.cwd();
```

- **开发模式**: `process.cwd()` = 仓库根目录,是真实项目,符合预期
- **打包模式**: `process.cwd()` = 安装目录
(`C:\Users\<u>\AppData\Local\Programs\AgentDock`),**不是**用户项目

renderer 启动调 `db:projects:list` → `syncProject(activeProjectPath)` →
项目在 global DB 找不到 → **auto-register** 用目录名 "AgentDock" 注册。
这就是 `AgentDock` 那一行的来源。

**修复**: 打包模式不 auto-init 项目,显示"打开项目"欢迎页。

```ts
if (!app.isPackaged) {
activeProjectPath = process.cwd(); // dev only
} else {
// packaged: no auto project, show welcome screen
}
```

### 根因 2: legacy `~/.agentdock/projects.db` 未被清理

v0.1-v0.2 时期 global projects DB 存在 `$HOME/.agentdock/projects.db`
(后来迁移到 userData,但 fallback 逻辑一直保留 homedir 路径)。

这个文件里积累了 41 行历史记录(e2e 测试目录、worktree 目录、build
产物目录都被当成 project 注册过)。**卸载不清、重装照读**,所以 tab
一直存在。

尝试用 NSIS `customUnInstall` / `customUnInstallSection` 宏在卸载时删
`~/.agentdock`,但踩了两个坑(见下)。最终改为**启动时清理**(JS 层,
100% 可靠):

```ts
// electron/main.ts, before openGlobalDb()
const legacyDbPath = join(app.getPath("home"), ".agentdock", "projects.db");
if (existsSync(legacyDbPath)) unlinkSync(legacyDbPath);
```

## NSIS 踩坑记录(重要)

### 坑 1: `$USERPROFILE` 不是 NSIS 内置变量

在 NSIS uninstaller 里写 `RMDir /r "$USERPROFILE\.agentdock"` **静默失败**。
NSIS 编译时报 warning(但不影响构建):

```
6000: unknown variable/constant "USERPROFILE" detected, ignoring
```

`$USERPROFILE` 是 Windows 环境变量,NSIS 不继承。合法的 NSIS 内置变量
只有 `$APPDATA`(Roaming)、`$LOCALAPPDATA`(Local)、`$DESKTOP`、
`$PROGRAMFILES` 等。`$PROGRAMDATA` 同样不是内置变量。

**正确做法**: 用 `$DESKTOP` 倒推 home 目录:

```nsi
StrCpy $0 "$DESKTOP" ; C:\Users\<u>\Desktop
StrCpy $0 $0 -8 ; 去掉末尾 "Desktop" (8 字符) → C:\Users\<u>\
StrCpy $0 "$0.agentdock" ; → C:\Users\<u>\.agentdock
```

### 坑 2: `customUnInstall` / `customUnInstallSection` 宏不展开

electron-builder 的 NSIS 模板用 `!ifmacrodef customUnInstall` 检测自定义
宏。但 NSIS 3.0.4 在某些情况下不识别 `.nsh` include 文件里的宏定义,
`!insertmacro` 展开为空。详见 `docs/tech-debt/nsis-macro-expansion.md`。

**结论**: 不要依赖 NSIS 宏做关键清理逻辑。userData 清理放在 **app 启动时
的 JS 层**(可靠),NSIS 宏只做锦上添花(`deleteAppDataOnUninstall: true`
删 Roaming,宏尝试删其它路径,能删就删,删不了靠 JS 兜底)。

## $APPDATA vs $LOCALAPPDATA vs 安装目录

三者是不同路径,容易混淆:

| NSIS 变量 | 实际路径 | 用途 |
|---|---|---|
| `$APPDATA` | `C:\Users\<u>\AppData\Roaming` | electron 默认 userData |
| `$LOCALAPPDATA` | `C:\Users\<u>\AppData\Local` | perUser 安装目录父级 |
| 安装目录 | `$LOCALAPPDATA\Programs\AgentDock` | exe + dll |
| legacy DB | `C:\Users\<u>\.agentdock` | v0.1-v0.2 global DB |

## 验证方法(无 GUI 自动化测试)

用 PowerShell/Python 做 silent install/uninstall(无需管理员,perUser):

```python
import subprocess, os, sqlite3
# 清旧 DB
db = os.path.expanduser('~/.agentdock/projects.db')
if os.path.exists(db): os.remove(db)
# silent 安装
subprocess.run([installer, '/S', '/currentuser', r'/D=C:\AgentDock-Test'])
# 启动 + 等窗口
p = subprocess.Popen([r'C:\AgentDock-Test\AgentDock.exe'])
time.sleep(15)
# 检查 global DB —— 应该是 0 行(打包后不 auto-register)
conn = sqlite3.connect(db)
print(conn.execute('SELECT COUNT(*) FROM projects').fetchone())
```

关键断言: **全新安装启动后 `projects.db` 应为 0 行**(或文件不存在)。
如果 > 0,说明 auto-register 又把某个非项目目录注册了。

## 修改清单

| 文件 | 改动 |
|---|---|
| `electron/main.ts` | 打包模式不 auto-init 项目;启动时清 legacy `~/.agentdock/projects.db` |
| `build/installer/installer.nsh` | `$DESKTOP` 倒推 home 删 `.agentdock`;customUnInstall + customUnInstallSection 双保险 |
| `electron-builder.yml` | `deleteAppDataOnUninstall: true`;include `build/installer/installer.nsh` |
| `docs/troubleshooting.md` | 用户手动清理指南 |
| `docs/tech-debt/nsis-macro-expansion.md` | NSIS 宏不展开的技术债记录 |
84 changes: 84 additions & 0 deletions docs/tech-debt/nsis-macro-expansion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Technical Debt: NSIS customInstall/customUnInstall macros don't expand

## Status: Known issue, deferred

## Symptom

When the AgentDock NSIS installer is built, macros defined in
`build/installer/custom-uninstall.nsh` are not expanded by
`!ifmacrodef` blocks in electron-builder's NSIS templates. The
`!ifmacrodef preInit` and `!ifmacrodef customUnInstall` blocks
remain empty in the generated `builder-debug.yml` — the
`!insertmacro` line produces no content.

## Reproduction

1. Build with the current `electron-builder.yml` (which includes
`build/installer/custom-uninstall.nsh` via `nsis.include`).
2. Inspect `release/0.2.0/builder-debug.yml`.
3. Search for `!ifmacrodef preInit` and `!ifmacrodef customUnInstall`.
4. The corresponding `!insertmacro` line has no content — the macro
was not recognized by NSIS 3.0.4.

## Root cause

Unknown. Hypotheses:

- NSIS 3.0.4 doesn't see `!macro` definitions in utf-8 .nsh files
(the file is valid ASCII, so this seems unlikely).
- electron-builder's include mechanism uses a different macro
namespace than expected.
- `!ifmacrodef` only matches macros defined in the *same file*, not
in included files (although my minimal repro shows this should
work).

## Workaround attempted

- Renamed `customUnInstallSection` to `customUnInstall` (the former
is expanded after `SectionEnd` where runtime commands are
invalid).
- Replaced template variables like `${INSTALL_REGISTRY_KEY}` with
hardcoded paths to avoid potential undefined-variable issues.
- Verified the file is plain ASCII with no BOM.

None of these fixed the expansion. The macros are still not seen
by `!ifmacrodef`.

## Impact

For the default perUser install path:

- `C:\ProgramData\AgentDock` is never created (perUser install
doesn't touch ProgramData).
- The legacy `InstallLocation` registry cache is not cleared by
the `preInit` macro. This means a previous perMachine install
leaves a stale default in the registry, which the next perUser
install reads. Verified manually: uninstalling v0.2.3 and
deleting `HKLM\SOFTWARE\AgentDock` before installing v0.3.0
yields the correct default path.

For the perMachine install path (rare for this project):

- `C:\ProgramData\AgentDock` is not cleaned at uninstall. Users
must manually delete the directory.

## Resolution options

| Option | Effort | Tradeoff |
|---|---|---|
| A. Accept current behavior, document workaround | 0 | Manual cleanup needed for perMachine users |
| B. Fork electron-builder NSIS template to inject runtime hooks | Medium (1-2 days) | Maintain fork across electron-builder upgrades |
| C. Switch to Inno Setup | High (3-5 days) | Lose electron-updater differential updates |
| D. Move cleanup to main-process uninstall wrapper | Medium (1 day) | Custom uninstall script; can call Node API |

## Decision

**Selected: A.** Defer to "when we actually need perMachine install
for non-trivial users". For now the perUser path is correct.

## Revisit when

- Any user reports a real perMachine install issue
- We decide to ship a true perMachine shared-data mode
- electron-builder changes its NSIS hook contract in a way that
makes our approach easier
78 changes: 78 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Troubleshooting

## UserData Locations

AgentDock stores its data based on install mode:

| Install mode | Path |
|---|---|
| perUser (default) | `%APPDATA%\AgentDock\` |
| perMachine | `%PROGRAMDATA%\AgentDock\` |

The global projects list (the tabs you see at the top) lives at:

| Install mode | Global projects DB |
|---|---|
| perUser | `%APPDATA%\AgentDock\global\projects.db` |
| perMachine | `%PROGRAMDATA%\AgentDock\global\projects.db` |

## Same project shows multiple tabs

If you see the same project name twice (e.g. "Copilot-Switch" appearing
twice), the global projects DB has duplicate entries with different path
spellings (e.g. trailing slash, mixed case, forward vs back slashes).

This was a bug in v0.2.x where the dedup only compared exact path strings.
**Fixed in v0.3.0+** — fuzzy match + path healing.

For older installs, see "Manual cleanup" below.

## Manual cleanup

If you have leftover data from an old version or a botched uninstall:

```cmd
:: Per-user data
rmdir /s /q "%APPDATA%\AgentDock"

:: Per-machine data (if installed perMachine)
rmdir /s /q "%PROGRAMDATA%\AgentDock"

:: Git worktree leftovers inside a project directory
rmdir /s /q "<project-path>\.agentdock\worktrees"
```

After cleanup, **reinstall v0.3.0+** which uses
`deleteAppDataOnUninstall: true` and a custom NSIS uninstall macro to
also clear `%PROGRAMDATA%\AgentDock` on perMachine installs.

## v0.3.0+ uninstall behavior

`deleteAppDataOnUninstall: true` plus the `customUnInstall` macro in
`build/installer/installer.nsh` clears:

1. `%APPDATA%\AgentDock\` (per-user appData)
2. `%PROGRAMDATA%\AgentDock\` (perMachine data, if it exists)

On upgrade, the install is preserved (no data wipe) — the
`${isUpdated}` define distinguishes fresh install from update.

## Git worktree cleanup

AgentDock's `syncProject` (called on every project open) runs
`git worktree prune` and removes incomplete worktree directories
(no `.git` pointer, left over from failed session creation). If you
still see weirdness, run from the project root:

```bash
git worktree prune
ls .agentdock/worktrees # check for leftover empty dirs
```

## NSIS custom-uninstall macro not expanding (technical debt)

If you see install-mode page still shows "for all users" or the default
install path is wrong, the NSIS macro in `build/installer/installer.nsh`
didn't expand at build time. See
`docs/tech-debt/nsis-macro-expansion.md` for details and the
documented workaround (the v0.2.3 release workflow used the macro).
Loading
Loading