Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
8c76fe1
fix(update): let the GUI updater's hermes update child pass the lock …
OutThisLife Jul 30, 2026
7556c51
fix(desktop): focus the tab restored by undo-close (⌘⇧T)
OutThisLife Jul 30, 2026
5affcd6
feat(desktop): open a folder as a project with ⌘O
OutThisLife Jul 30, 2026
a11611a
feat(desktop): projects in the command palette
OutThisLife Jul 30, 2026
ba194c1
feat(desktop): File > Open Folder… menu item
OutThisLife Jul 30, 2026
53f7d13
fix(windows): native Windows correctness for CLI, gateway status, ban…
lEWFkRAD Jul 30, 2026
8e1debd
docs: purge stale xdist/_enforce_test_timeout test-runner references …
konsisumer Jul 30, 2026
958ab81
fix(desktop): sidebar labels truncate instead of pushing header icons…
OutThisLife Jul 30, 2026
3209236
fix(desktop): fade the ⌘-held project label
OutThisLife Jul 30, 2026
fa8b959
Merge pull request #74630 from NousResearch/bb/update-restart-race
OutThisLife Jul 30, 2026
c5a6821
feat(desktop): recognize slash commands in text the composer didn't w…
OutThisLife Jul 30, 2026
ccca952
feat(desktop): chip pasted directives in the composer
OutThisLife Jul 30, 2026
422ecfe
feat(desktop): hydrate commands when repainting inert composer text
OutThisLife Jul 30, 2026
1e2d550
Merge pull request #74642 from NousResearch/bb/sidebar-icon-clip
OutThisLife Jul 30, 2026
05f5df6
Merge pull request #74640 from NousResearch/bb/undo-close-tab-focus
OutThisLife Jul 30, 2026
0bf471d
fix(gateway): preserve voice_only semantics for text input
juanmartitegui Jul 29, 2026
a2a74d7
Merge pull request #74623 from NousResearch/bb/open-project
OutThisLife Jul 30, 2026
866c9ad
Merge pull request #74651 from NousResearch/bb/composer-paste-directives
OutThisLife Jul 30, 2026
f1a91ad
fix(tui): split terminal tab title from window title
OutThisLife Jul 30, 2026
fed48cf
fmt(js): `npm run fix` on merge (#74677)
hermes-seaeye[bot] Jul 30, 2026
975f4ef
perf(desktop): budget the transcript live tail in parts, not turns
OutThisLife Jul 30, 2026
c55159f
Merge pull request #74683 from NousResearch/bb/osc-tab-title
OutThisLife Jul 30, 2026
0481766
perf(desktop): make ⌘K cost nothing until it opens
OutThisLife Jul 30, 2026
b5ca900
fmt(js): `npm run fix` on merge (#74694)
hermes-seaeye[bot] Jul 30, 2026
d437d3e
Merge pull request #74665 from NousResearch/bb/palette-perf
OutThisLife Jul 30, 2026
9d589b9
refactor(desktop): one hook for a message's reactions
OutThisLife Jul 30, 2026
b69d4e5
feat(desktop): double-click a message to heart it
OutThisLife Jul 30, 2026
2c71a37
fix(desktop): clear double-click heart lint errors
OutThisLife Jul 30, 2026
9accf79
fmt(js): `npm run fix` on merge (#74702)
hermes-seaeye[bot] Jul 30, 2026
dd241cf
Merge pull request #74644 from NousResearch/bb/double-tap-heart
OutThisLife Jul 30, 2026
d405b4f
fix(kanban): persist dispatcher health signal
SSC-ENG Jul 29, 2026
e934207
fix(kanban): harden dispatcher health and PR guard
SSC-ENG Jul 29, 2026
aae1430
test(kanban): restore dispatcher health regressions
SSC-ENG Jul 30, 2026
17a5f95
fix(kanban): surface missing assignee profiles
Jul 29, 2026
7ec806f
fix(kanban): honor explicit requeue after PR evidence
SSC-ENG Jul 29, 2026
8434f91
fix(kanban): fold HAA option-B hotfix into consolidated branch
SSC-ENG Jul 30, 2026
2d3f4d5
feat(kanban): classify worker deaths loudly from worker-log evidence
SSC-ENG Jul 30, 2026
ac559d0
test(kanban): scope respawn-guard PR fixtures to code tasks
SSC-ENG Jul 30, 2026
a71fcf6
test(kanban): regression test - active_pr guard scoped to code tasks
SSC-ENG Jul 30, 2026
60cb8cc
merge: resolve conflicts vs fork/main for PR #13
SSC-ENG Jul 30, 2026
24e8270
test(kanban): fix merge-lane fixtures for fork/main preflight gate
SSC-ENG Jul 30, 2026
575fb1b
fix(kanban): restore unconditional per-board health probe dropped by …
SSC-ENG Jul 30, 2026
c2cbb10
test(kanban): restore WAL-reset-vulnerability monkeypatch dropped by …
SSC-ENG Jul 30, 2026
64b12e7
Merge remote-tracking branch 'origin/main' into fix/t-543dce5d-dispat…
Jul 31, 2026
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
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1284,14 +1284,15 @@ def profile_env(tmp_path, monkeypatch):
### Python
**ALWAYS use `scripts/run_tests.sh`** — do not call `pytest` directly. The script enforces
hermetic environment parity with CI (unset credential vars, TZ=UTC, LANG=C.UTF-8,
`-n auto` xdist workers, in-tree subprocess-isolation plugin). Direct `pytest`
per-file subprocess isolation via `scripts/run_tests_parallel.py` — no xdist,
worker count auto-scaled from CPU count). Direct `pytest`
on a 16+ core developer machine with API keys set diverges from CI in ways
that have caused multiple "works locally, fails in CI" incidents (and the reverse).

```bash
scripts/run_tests.sh # full suite, CI-parity
scripts/run_tests.sh tests/gateway/ # one directory
scripts/run_tests.sh tests/agent/test_foo.py::test_x # one test
scripts/run_tests.sh tests/agent/test_foo.py -k test_x # one test (file + -k; the runner is file-granular)
scripts/run_tests.sh -v --tb=long # pass-through pytest flags
```

Expand Down
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@ ln -sf "$(pwd)/venv/bin/hermes" ~/.local/bin/hermes
### Run tests

```bash
# Preferred — matches CI (hermetic env, 4 xdist workers); see AGENTS.md
# Preferred — matches CI (hermetic `env -i`, per-file subprocess isolation
# via run_tests_parallel.py, worker count auto-scaled); see AGENTS.md
scripts/run_tests.sh

# Alternative (activate the venv first). The wrapper is still recommended
Expand Down Expand Up @@ -848,7 +849,7 @@ that touches the OS, assume *any* platform can hit your code path.
Tests that use POSIX-only syscalls need a skip marker. Common ones:
- Symlinks → `@pytest.mark.skipif(sys.platform == "win32", ...)`
- `0o600` file modes → `@pytest.mark.skipif(sys.platform.startswith("win"), ...)`
- `signal.SIGALRM` → Unix-only (see `tests/conftest.py::_enforce_test_timeout`)
- `signal.SIGALRM` → Unix-only (per-test timeouts no longer use it directly; see the win32 timeout-method shim in `tests/conftest.py::pytest_configure`)
- `os.setsid` / `os.fork` → Unix-only
- Live Winsock / Windows-specific regression tests →
`@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific regression")`
Expand Down
22 changes: 22 additions & 0 deletions apps/bootstrap-installer/src-tauri/src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,17 @@ fn update_child_env(install_root: &Path) -> Vec<(String, OsString)> {
// a frozen stage, and users cancel a healthy update. Force line-by-line
// output instead.
envs.push(("PYTHONUNBUFFERED".to_string(), OsString::from("1")));
// We hold the update-in-progress marker for this whole run, and the
// `hermes update` child claims that SAME lock (hermes_cli/update_lock.py).
// Name our pid so the child recognizes the live holder as its own
// orchestrator and runs under our claim — without this every GUI update
// refuses its parent's marker with exit 2 ("Hermes is still running")
// and no number of retries can ever succeed. Keep the variable name in
// sync with HANDOFF_PID_ENV in hermes_cli/update_lock.py.
envs.push((
"HERMES_UPDATE_HANDOFF_PID".to_string(),
OsString::from(std::process::id().to_string()),
));
if let Some(path) = path_with_prepended_entries(&[
hermes_home.join("node").join("bin"),
venv_bin_dir(install_root),
Expand Down Expand Up @@ -1218,6 +1229,17 @@ mod tests {
);
}

#[test]
fn update_child_env_names_our_pid_for_the_lock_handoff() {
let envs = update_child_env(Path::new("/x/hermes-agent"));
assert!(
envs.iter().any(|(k, v)| k == "HERMES_UPDATE_HANDOFF_PID"
&& v.to_str() == Some(std::process::id().to_string().as_str())),
"the hermes update child claims the same marker we hold; without our pid \
it refuses its own parent's lock and every GUI update dead-ends on exit 2"
);
}

#[test]
fn lock_probe_paths_include_desktop_app_payload() {
let root = Path::new("/x/hermes-agent");
Expand Down
18 changes: 18 additions & 0 deletions apps/desktop/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5155,6 +5155,20 @@ function sendClosePreviewRequested() {
webContents.send('hermes:close-preview-requested')
}

function sendOpenFolderRequested() {
if (!mainWindow || mainWindow.isDestroyed()) {
return
}

const webContents = mainWindow.webContents

if (!webContents || webContents.isDestroyed()) {
return
}

webContents.send('hermes:open-folder-requested')
}

// Tell the renderer the machine just woke. Sleep silently drops the
// renderer's WebSocket to the local backend; the renderer reconnects on this
// signal so the chat composer doesn't stay stuck on "Starting Hermes...".
Expand Down Expand Up @@ -5272,6 +5286,10 @@ function buildApplicationMenu() {
// a menu accelerator would fight the rebind panel and (on macOS) be
// swallowed before the renderer sees it. Here purely for discoverability.
{ click: () => createInstanceWindow(), label: 'New Window' },
// Same no-accelerator rationale: ⌘O is the rebindable renderer keybind
// (workspace.openFolder). Clicking runs the same open-folder-as-project
// flow through the renderer.
{ click: () => sendOpenFolderRequested(), label: 'Open Folder…' },
{ type: 'separator' },
IS_MAC
? {
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,12 @@ contextBridge.exposeInMainWorld('hermesDesktop', {

return () => ipcRenderer.removeListener('hermes:close-preview-requested', listener)
},
onOpenFolderRequested: callback => {
const listener = () => callback()
ipcRenderer.on('hermes:open-folder-requested', listener)

return () => ipcRenderer.removeListener('hermes:open-folder-requested', listener)
},
onOpenUpdatesRequested: callback => {
const listener = () => callback()
ipcRenderer.on('hermes:open-updates', listener)
Expand Down
146 changes: 146 additions & 0 deletions apps/desktop/scripts/probe-command-palette.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// ⌘K open latency, measured in-page (no CDP round-trip in the number).
//
// node scripts/probe-command-palette.mjs [--port 9222] [--rounds 8]
//
// Reports, per round, the time from the keydown the app actually receives to:
// frame_ms — the dialog frame + input in the DOM and painted (what "instant"
// means: the overlay owes you a frame immediately)
// rows_ms — the row list painted (may lag frame_ms; rows are deferred)
// plus any long tasks in the window, so a slow open is attributable.
import { CDP, sleep } from './perf/lib/cdp.mjs'

const args = process.argv.slice(2)
const flag = name => {
const i = args.indexOf(`--${name}`)

return i >= 0 ? args[i + 1] : undefined
}

const port = Number(flag('port') ?? 9222)
const rounds = Number(flag('rounds') ?? 8)

const cdp = await CDP.connect({ port })

await cdp.send('Runtime.enable')

const INSTALL = `
(() => {
if (window.__CMDK__) window.__CMDK__.stop()

const state = { t0: null, frame: null, rows: 0, rowsAt: null, tasks: [], armed: false }

// Time from the keydown the APP receives — excludes CDP transport, so the
// number is what a user's finger actually experiences.
const onKey = e => {
if (state.armed && (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
state.t0 = performance.now()
state.armed = false
}
}

window.addEventListener('keydown', onKey, true)

const obs = new MutationObserver(() => {
if (state.t0 === null) return
if (state.frame === null && document.querySelector('[cmdk-input]')) {
state.frame = performance.now() - state.t0
}
const n = document.querySelectorAll('[cmdk-item]').length
if (n > state.rows) { state.rows = n; state.rowsAt = performance.now() - state.t0 }
})

obs.observe(document.body, { childList: true, subtree: true })

const po = new PerformanceObserver(list => {
for (const e of list.getEntries()) state.tasks.push({ start: e.startTime, dur: Math.round(e.duration) })
})

try { po.observe({ entryTypes: ['longtask'] }) } catch {}

window.__CMDK__ = {
arm: () => { state.t0 = null; state.frame = null; state.rows = 0; state.rowsAt = null; state.tasks = []; state.armed = true },
read: () => ({
frame_ms: state.frame === null ? -1 : Math.round(state.frame),
rows_ms: state.rowsAt === null ? -1 : Math.round(state.rowsAt),
rows: state.rows,
longtask_ms: state.t0 === null ? 0 : state.tasks.filter(t => t.start >= state.t0).reduce((s, t) => s + t.dur, 0)
}),
stop: () => { window.removeEventListener('keydown', onKey, true); obs.disconnect(); po.disconnect() }
}

return true
})()
`

// Settle: frame painted AND rows stopped growing for two frames.
const WAIT = `
new Promise(resolve => {
let stable = 0
let last = -1
const started = performance.now()
const tick = () => {
const r = window.__CMDK__.read()
if (r.frame_ms >= 0 && r.rows === last && r.rows > 0) {
if (++stable >= 2) { resolve(r); return }
} else { stable = 0 }
last = r.rows
if (performance.now() - started > 8000) { resolve(window.__CMDK__.read()); return }
requestAnimationFrame(tick)
}
requestAnimationFrame(tick)
})
`

const key = async type =>
cdp.send('Input.dispatchKeyEvent', {
type,
key: 'k',
code: 'KeyK',
windowsVirtualKeyCode: 75,
nativeVirtualKeyCode: 75,
modifiers: 4
})

const esc = async () => {
for (const type of ['keyDown', 'keyUp']) {
await cdp.send('Input.dispatchKeyEvent', { type, key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 })
}

await sleep(400)
}

await cdp.eval(INSTALL)
await esc()

const samples = []

for (let i = 0; i < rounds; i++) {
await sleep(250)
await cdp.eval('window.__CMDK__.arm()')
await key('rawKeyDown')
await key('keyUp')
const r = await cdp.eval(WAIT)
samples.push(r)
console.log(`round ${i}:`, r)
await esc()
}

await cdp.eval('window.__CMDK__.stop()')

const stat = k => {
const v = samples.map(s => s[k]).filter(n => n >= 0).sort((a, b) => a - b)

if (!v.length) return null

return {
min: v[0],
median: v[Math.floor(v.length / 2)],
max: v[v.length - 1]
}
}

console.log('\nkeydown → dialog frame painted (ms):', stat('frame_ms'))
console.log('keydown → rows painted (ms):', stat('rows_ms'))
console.log('long-task time in window (ms):', stat('longtask_ms'))

cdp.close()
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
const editor = editorRef.current

if (editor) {
renderComposerContents(editor, next)
renderComposerContents(editor, next, { trailingCommitted: true })
placeCaretEnd(editor)
}

Expand Down Expand Up @@ -265,7 +265,7 @@
const editor = editorRef.current

if (editor && document.activeElement !== editor && composerPlainText(editor) !== text) {
renderComposerContents(editor, text)
renderComposerContents(editor, text, { trailingCommitted: true })
}

if (isBrowsingHistory(sessionIdRef.current) || queueEditRef.current) {
Expand Down Expand Up @@ -297,7 +297,7 @@
unsubscribe()
window.clearTimeout(draftPersistTimerRef.current)
}
}, [composerRuntime, queueEditRef])

Check warning on line 300 in apps/desktop/src/app/chat/composer/hooks/use-composer-draft.ts

View workflow job for this annotation

GitHub Actions / JS & TS checks / apps/desktop / check:lint

React Hook useEffect has a missing dependency: 'stashAt'. Either include it or remove the dependency array

const insertText = (text: string) => {
const base = draftRef.current
Expand Down Expand Up @@ -401,7 +401,7 @@
window.removeEventListener('pagehide', flushPendingDraftPersist)
flushPendingDraftPersist()
}
}, [syncDraftFromEditor])

Check warning on line 404 in apps/desktop/src/app/chat/composer/hooks/use-composer-draft.ts

View workflow job for this annotation

GitHub Actions / JS & TS checks / apps/desktop / check:lint

React Hook useEffect has a missing dependency: 'stashAt'. Either include it or remove the dependency array

return {
activeQueueSessionKeyRef,
Expand Down
Loading
Loading