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
3 changes: 2 additions & 1 deletion .ai/contexts/ipc-bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ every call, and the absolute path built from it is used and discarded there.
| `open-external` | Opens https:// URLs in OS browser |
| `clipboard-write-text` | Main-process clipboard write (Wayland fix, PR #18) |
| `get-app-version` | From package.json |
| `whats-new-startup` / `whats-new-dismissed` | The What's new dialog: the `CHANGELOG.md` sections to show on startup (or `null`), and recording the running version as `lastSeenVersion` when it closes. Help → What's new sends `show-whats-new` with the running version's section. See `docs/changelog.md` |
| `updater-check` / `updater-download` / `updater-install` | electron-updater |

### Send (fire-and-forget, renderer → main)
Expand All @@ -136,7 +137,7 @@ every call, and the absolute path built from it is used and discarded there.

### Events (main → renderer)

`terminal-data`, `session-detected`, `process-exited`, `terminal-notification`, `cli-busy-state`, `session-forked`, `subagent-spawned`, `subagent-completed`, `subagent-watch-event`, `projects-changed`, `status-update`, `indexing-progress`, `file-changed`, `mcp-open-diff`, `mcp-open-file`, `mcp-close-all-diffs`, `mcp-close-tab`, `updater-event`, `session-transcript-activity`
`terminal-data`, `session-detected`, `process-exited`, `terminal-notification`, `cli-busy-state`, `session-forked`, `subagent-spawned`, `subagent-completed`, `subagent-watch-event`, `projects-changed`, `status-update`, `indexing-progress`, `file-changed`, `mcp-open-diff`, `mcp-open-file`, `mcp-close-all-diffs`, `mcp-close-tab`, `updater-event`, `show-whats-new`, `session-transcript-activity`

`session-transcript-activity` and (not listed above; see
`.ai/contexts/session-cache.md`, "Remote hosts — busy spinner") `remote-activity`
Expand Down
4 changes: 2 additions & 2 deletions .ai/contexts/window-frame.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ the same strip across the rest of the window.

| File | Role |
|---|---|
| `window-frame.js` | `windowFrameOptions(platform)` (the `BrowserWindow` options), `applicationMenuTemplate(appName)` (the menu), `KEYBOARD_ROLES`, `STRIP_HEIGHT`, `zoomKey()` and `nextZoomLevel()` (the zoom keys), `menuPopupPoint()` (where the menu button pops the menu up). |
| `window-frame.js` | `windowFrameOptions(platform)` (the `BrowserWindow` options), `applicationMenuTemplate(appName, { onWhatsNew })` (the menu; Help → What's new calls `onWhatsNew`, see `docs/changelog.md`), `KEYBOARD_ROLES`, `STRIP_HEIGHT`, `zoomKey()` and `nextZoomLevel()` (the zoom keys), `menuPopupPoint()` (where the menu button pops the menu up). |
| `main.js` | Spreads `windowFrameOptions(process.platform)` into the `BrowserWindow`; `buildMenu()` installs the template; the `popup-app-menu` IPC opens it under the menu button. |
| `preload.js` | `window.api.popupAppMenu(x, y)`. |
| `public/window-strip.js` | Marks `<body>` with `window-frameless`, `platform-<os>` and, while full screen, `window-full-screen`; wires `#app-menu-btn`. Dual-mode: a classic `<script>`, `require()`-d by the test. |
Expand Down Expand Up @@ -78,7 +78,7 @@ inside a drag region never receives the mouse. `no-drag` is therefore set on:
every session-header control (`#terminal-header-controls [data-header-kind]`),
`#jsonl-viewer-session-id`, `.viewer-toolbar-path`;
- every overlay that can open over the strip: `.new-session-popover`,
`.terminal-context-menu`, `.new-session-overlay`, `.add-project-overlay`,
`.terminal-context-menu`, `.new-session-overlay`, `.add-project-overlay`, `.whats-new-overlay`,
`.jsonl-screenshot-fullscreen`, `#update-toast`, `.restore-toast`. A new
popover, menu or dialog that can reach the top 32 pixels belongs in this list.

Expand Down
12 changes: 7 additions & 5 deletions .ai/shared-guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Switchboard is an **Electron desktop app**: renderer + main-process, no Domain/A
| Working practices for AI agents (HANDOFF format, shell pitfalls, review loop) | [agent-practices.md](agent-practices.md) |
| Test a PR or a release candidate against a running app | [../docs/testing-a-pr.md](../docs/testing-a-pr.md) |
| Cut a release | [docs/releasing.md](../docs/releasing.md) — and its fork gotchas, which are not optional |
| Write a `CHANGELOG.md` entry, or change the changelog CI check or the What's new dialog | [docs/changelog.md](../docs/changelog.md) |
| Drive a live instance that cannot see the user's sessions | [../docs/live-testing.md](../docs/live-testing.md) |

For a guided tour of the codebase architecture, start at [contexts/README.md](contexts/README.md).
Expand Down Expand Up @@ -154,11 +155,12 @@ These exist on `devsuitup/switchboard` main but not on `doctly/switchboard` main
never in the code. What may remain in code: at most a one-line pointer to
that doc (e.g. `// see .ai/contexts/subagent-observability.md`), and that's
the ceiling (maintainer rule, PRs #127/#130).
2. `task check` (lint + test). 0 errors. Pre-existing warnings are fine.
3. Squash to clear commits. No `Co-Authored-By`. Imperative subject, brief why-body.
4. `gh pr create` against `devsuitup/switchboard:main` (the fork's main, not upstream's). Title format: `(area): short imperative`.
5. If the change is a port of an upstream PR, **credit the upstream author** in the body with a link. We want abasiri to see we're not stealing.
6. **When the PR is ready to merge** (internal review loop converged to zero findings, or an external PR judged mergeable after review), **request the maintainer account `devsuitup` as reviewer**: `gh api -X POST repos/devsuitup/switchboard/pulls/<n>/requested_reviewers -f 'reviewers[]=devsuitup'`. The maintainer's review queue is the single list of what awaits approval — a ready PR that never requests review sits invisible.
2. **Changelog.** Every PR that changes behaviour adds its entry under `## Unreleased` in `CHANGELOG.md`, written to the rule in [docs/changelog.md](../docs/changelog.md); a PR users see nothing of takes the `no-changelog` label instead.
3. `task check` (lint + test). 0 errors. Pre-existing warnings are fine.
4. Squash to clear commits. No `Co-Authored-By`. Imperative subject, brief why-body.
5. `gh pr create` against `devsuitup/switchboard:main` (the fork's main, not upstream's). Title format: `(area): short imperative`.
6. If the change is a port of an upstream PR, **credit the upstream author** in the body with a link. We want abasiri to see we're not stealing.
7. **When the PR is ready to merge** (internal review loop converged to zero findings, or an external PR judged mergeable after review), **request the maintainer account `devsuitup` as reviewer**: `gh api -X POST repos/devsuitup/switchboard/pulls/<n>/requested_reviewers -f 'reviewers[]=devsuitup'`. The maintainer's review queue is the single list of what awaits approval — a ready PR that never requests review sits invisible.

## Upstreaming work

Expand Down
14 changes: 5 additions & 9 deletions .claude/commands/release.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
Perform a release for this project, following [docs/releasing.md](../../docs/releasing.md). Always push with an explicit remote (`git push origin …`): a clone whose `main` tracks `upstream` would send a bare `git push` to `doctly/switchboard`. Steps:

1. Find the most recent version tag with `git fetch origin --tags && git describe --tags --abbrev=0 origin/main`, and read the commits since it: `git log {prev_tag}..origin/main --format="%B---"`.
2. Bump the version on a release branch, never on `main` (the ruleset rejects direct pushes):
1. Find the most recent version tag with `git fetch origin --tags && git describe --tags --abbrev=0 origin/main`, and read the commits since it: `git log {prev_tag}..origin/main --format="%B---"`. Check that every user-visible change among them has its entry under `## Unreleased` in `CHANGELOG.md` (the rule is in [docs/changelog.md](../../docs/changelog.md)); if one is missing, show the user the entry you would add and add it in the bump PR.
2. Bump the version on a release branch, never on `main` (the ruleset rejects direct pushes). In `CHANGELOG.md`, rename `## Unreleased` to `## v{version} — {today, YYYY-MM-DD}` and open a new, empty `## Unreleased` above it:
```bash
git checkout -b release/v{version} origin/main
npm version patch --no-git-tag-version # or the version asked for
# edit CHANGELOG.md as above
git commit -am "v{version}"
git push origin release/v{version}
```
Expand All @@ -28,14 +29,9 @@ Perform a release for this project, following [docs/releasing.md](../../docs/rel
git push origin v{version}
```
Pushing the tag starts `.github/workflows/build.yml`.
7. Watch the build: `gh run list --repo devsuitup/switchboard --workflow build.yml --limit 1`, then `gh run watch <id> --repo devsuitup/switchboard`. Its publish job creates a draft release, uploads the assets, and writes the release notes from the commit subjects since the previous tag.
7. Watch the build: `gh run list --repo devsuitup/switchboard --workflow build.yml --limit 1`, then `gh run watch <id> --repo devsuitup/switchboard`. Its publish job creates a draft release, uploads the assets, and writes the release notes: the tag's `CHANGELOG.md` section, then a full-changelog compare link. It fails if that section is missing or empty.
8. Check that all 19 assets are on the draft (`gh release view v{version} --repo devsuitup/switchboard --json assets --jq '.assets | length'`); upload any missing one with `gh release upload v{version} <file> --clobber --repo devsuitup/switchboard`.
9. Keep the notes the workflow wrote. To add a summary on top, read them first and write the result back with both:
```bash
gh release view v{version} --repo devsuitup/switchboard --json body --jq .body > notes.md
# prepend a grouped summary (Features, Bug Fixes, …) covering every commit, keep the generated list below it
gh release edit v{version} --repo devsuitup/switchboard --notes-file notes.md
```
9. Keep the notes the workflow wrote: they are the changelog section the installed apps show in their What's new dialog, and the two must say the same thing. Check the body (`gh release view v{version} --repo devsuitup/switchboard --json body --jq .body`); a wording to change is changed in `CHANGELOG.md`, through a PR. If the notes step failed, add the missing section through a PR, then fill in the draft by hand: `node scripts/changelog-section.js v{version} > notes.md` on the merged `main`, and `gh release edit v{version} --repo devsuitup/switchboard --notes-file notes.md`.
10. Publish the draft: `gh release edit v{version} --repo devsuitup/switchboard --draft=false --latest`.

If a build started from a wrong tag, cancel it (`gh run cancel <id>`), delete the tag locally and on `origin`, and tag the merged commit.
30 changes: 8 additions & 22 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
# Full history + tags: the release notes are derived from the commit range
# between the previous tag and this one.
# Full history + tags: only to find PREV_TAG for the notes' compare link.
- uses: actions/checkout@v4
with:
fetch-depth: 0
Expand Down Expand Up @@ -141,9 +140,9 @@ jobs:
done
exit $rc

# v0.0.63 to v0.0.65 all shipped with an empty body: the step above creates
# the draft with --notes "" and nothing ever filled it in. Derive the body
# from the tag range instead, and fail loudly rather than write an empty one.
# The step above creates the draft with --notes "": the body is the tag's
# CHANGELOG.md section, and the step fails rather than write an empty one.
# see docs/releasing.md
- name: Fill in release notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -153,23 +152,10 @@ jobs:
# The tag that precedes this one on this tag's own history, not the
# repository's newest tag.
PREV_TAG="$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || true)"
if [ -n "$PREV_TAG" ]; then RANGE="${PREV_TAG}..${TAG}"; else RANGE="$TAG"; fi
# main is a chain of PR squash-merges, one subject per PR. The version
# bump commit ("v0.0.65 (#189)") is release plumbing, not a change.
git log --no-merges --format='%s' "$RANGE" \
| grep -Ev '^v[0-9]+\.[0-9]+\.[0-9]+([^0-9].*)?$' > subjects.txt || true
if [ ! -s subjects.txt ]; then
echo "::error::no release-worthy commits in ${RANGE}; refusing to write an empty release body"
exit 1
node scripts/changelog-section.js "$TAG" > release-notes.md
if [ -n "$PREV_TAG" ]; then
echo >> release-notes.md
echo "**Full changelog**: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/compare/${PREV_TAG}...${TAG}" >> release-notes.md
fi
{
echo "## What's changed"
echo
sed 's/^/- /' subjects.txt
if [ -n "$PREV_TAG" ]; then
echo
echo "**Full changelog**: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/compare/${PREV_TAG}...${TAG}"
fi
} > release-notes.md
# Stays a draft: edit only replaces the body.
gh release edit "$TAG" --notes-file release-notes.md
15 changes: 15 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ jobs:
- name: Run ESLint
run: npm run lint

changelog:
# see docs/changelog.md ("The CI check")
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Require a CHANGELOG.md entry for app changes
env:
GH_TOKEN: ${{ github.token }}
run: node scripts/check-changelog.js --base "${{ github.event.pull_request.base.sha }}" --head "${{ github.event.pull_request.head.sha }}" --repo "${{ github.repository }}" --pr "${{ github.event.pull_request.number }}"

test:
runs-on: ${{ matrix.os }}

Expand Down
82 changes: 82 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Changelog

What changes for you in each release of Switchboard. How to write an entry: [docs/changelog.md](docs/changelog.md).

## Unreleased

### New
- After an update, a "What's new" dialog lists the changes of every version since the one you last ran. Help → What's new opens it again for the current version. (#363)
- An untracked file's line counts show in the Changes list without opening it, and count in the header total. A row that cannot be counted says why: binary, too large, not counted, or count on open for a remote session. (#350)

### Changed
- The session header's controls sit in one row with one look: Sandbox and IDE Emulation are plain indicators, Shell and Changes are icon buttons that show when they are on, and Stop comes last. (#348)
- Whether the session runs is a dot before its name, and its tooltip says how the process ended: stopped, killed, or exited with a code. (#348)
- The Changes panel and its editor use the app's own look: icon buttons, the dark surfaces of the sidebar, a badge for each file's state, and softer diff colours. (#351)

### Fixed
- Saving from the file panel, the Memory panel or the MCP diff tab no longer overwrites a file that changed on disk since you opened it. You are told, and asked before your edits replace it. (#355)
- When an open file changes on disk, the panel reloads it if you have no unsaved edits, and otherwise keeps them and offers Reload or Keep my edits. It keeps noticing changes after the file is replaced, deleted or recreated. (#355)
- Switching tabs or sessions no longer drops a file tab's unsaved edits. (#355)
- A save that fails says "Save failed" instead of failing silently, including one that finishes after you switched to another tab. (#355)
- Once an MCP diff has been accepted or rejected, its tab's Save button is disabled, and its tooltip says why. (#355)
- Undo no longer brings back an edit made in another file, or before the file was reloaded. (#355)
- The Changes panel works when a session's directory is below the repository root; tracked files' diffs came back empty there. (#350)
- A file with a merge conflict is badged `U` (Unmerged) in the Changes list, instead of Added or Deleted. (#351)
- A plain terminal no longer types its `claude` shim into the shell, so the line stays out of your shell history and off the screen. (#352)
- Archiving a session no longer moves its subagents into "Orphan subagents". (#354)

## v0.0.84 — 2026-09-29

### New
- The window draws its own title bar: the sidebar's top row holds a ☰ menu button, and the window controls sit in the top-right corner. The window gains the height the frame took, and every menu shortcut still works. (#338, #341)
- Ctrl+- and Ctrl+0 zoom on any keyboard layout, AZERTY included, and so do the numeric keypad's +, - and 0. (#338)

### Fixed
- A session running in another process, another Switchboard or a CLI in a terminal, is no longer resumed automatically on a reload or a restore. A notice names the process, and opening the session by hand asks first. (#337, #343)

## v0.0.83 — 2026-09-29

### New
- Switchboard can report to a local ActivityWatch server the session you are looking at and every session that runs, without any prompt or transcript content. It is off by default: turn it on in Settings → Activity Reporting. (#326)

### Fixed
- A right-click in a session's terminal no longer pastes the clipboard into Claude Code's prompt outside Native mode, and the terminal keeps its focus. (#330)
- On Windows, the activity trace no longer leaves a rotated file behind. (#328)

## v0.0.82 — 2026-09-22

### New
- A path or a filename in terminal output is a link that opens the file in the side panel, at the line for `path:line`. Only a file the panel can open is underlined. (#319)

### Fixed
- The settings panel shows the value a new session gets. IDE Emulation showed as on while sessions started with it off. (#317)

## v0.0.81 — 2026-09-18

### New
- Click a file in the Changes list to edit it in place, under the list, with the diff updated as you type: inline, side by side, or plain. A save onto a file that changed underneath you is refused, not merged. (#302)
- A shell under the Changes list runs in the session's own directory, worktree included. It is not a session: it never shows in the sidebar. (#300, #305)
- Untracked files are listed in Changes with a real diff and line counts. (#298)

### Changed
- The Changes panel always opens, and says "No changes" or that the directory is not a git repository, instead of showing git's own error. (#305, #310)

### Fixed
- An exited plain terminal reopens as a terminal, not as a Claude session with no transcript. (#305)
- The sidebar no longer overflows the window by the status bar's height, which could clip the top row of icons until a reload. (#306)

## v0.0.80 — 2026-09-17

### Changed
- A `schedule-*.md`, a `CLAUDE.md` or a memory note reached through a symlink is listed in the brain tab, and a linked schedule can be run from it. (#294)
- A project-root `CLAUDE.md`, `GEMINI.md` or `agents.md` that links to a file outside every open project and outside `~/.claude` is no longer listed. To list it again, move the link's target into a directory you have opened as a project. (#296)

### Fixed
- A FIFO, a socket or a device named `CLAUDE.md` in a project no longer freezes the app. (#296)
- A file reached through a symlink is checked against the credential denylist before it is read, so a linked key or `.env` no longer reaches the search index. (#294, #296)
- An unreadable file in a scanned directory no longer hides the files listed after it. (#296)
- An attached remote session's row shows the session's title. (#293)

---

Older versions: see [GitHub Releases](https://github.com/devsuitup/switchboard/releases).
Loading
Loading