Skip to content

release: 1.1.8#410

Merged
fujibee merged 1 commit into
mainfrom
release/v1.1.8
Jul 15, 2026
Merged

release: 1.1.8#410
fujibee merged 1 commit into
mainfrom
release/v1.1.8

Conversation

@fujibee

@fujibee fujibee commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Automated release of 1.1.8.

  • VERSION bump + synced package.json / .claude-plugin/plugin.json
  • CHANGELOG.md regenerated from Conventional Commits

Merging this and pushing v1.1.8 triggers the publish pipeline (npm + GitHub Release).

@fujibee fujibee merged commit 4b25697 into main Jul 15, 2026
7 checks passed
@fujibee fujibee deleted the release/v1.1.8 branch July 15, 2026 21:39
orangewk added a commit to orangewk/agmsg that referenced this pull request Jul 16, 2026
* Role-to-session affinity: named sessions, resume-by-role boot, tmux-resurrect (#339) (#344)

* feat(role-session): record (team, agent) -> session at actas-claim (#339)

Add scripts/lib/role-session.sh: an advisory (team, agent) -> session
record keyed on the BARE session id (stable across resume generations,
unlike the composite instance-id the actas lock keys on). It shares the
actas-lock filename encoding and run/ location; every write is best-effort
(a failed record write never fails the claim) and every read is fail-open.

actas-claim.sh writes the record on each successful claim, and none on the
held/rollback path (the record means 'last session that held this role',
not 'current owner' -- the lock answers that). Add agmsg_instance_bare_sid()
to instance-id.sh to extract the bare sid from a composite token.

First building block of role->session affinity: the moment a role is
actas'd it becomes resumable back into its prior context.

* feat(spawn): name spawned sessions <team>-<agent> via name_arg (#339)

Add a 'name_arg=' manifest key (claude-code: -n). spawn.sh's direct-CLI
boot script now emits '<name_arg> <team>-<agent>' when the type declares it,
so a spawned session is born named after its role -- meaningful in the prompt
box and the /resume picker, and the marker the tmux-resurrect hook (PR-D)
keys on. SESSION_NAME is computed after team resolution (a project-resolved
--team is only known then). Types without name_arg skip naming (unchanged);
the node-launcher branch is untouched (launchers own their CLI invocation).

Docs: the claude-code actas flow now suggests '/rename <team>-<agent>' for
sessions not launched via spawn, so the picker and pane titles stay meaningful.

* feat(spawn): resume-or-fresh boot for a role's prior session (#339)

When a role has a recorded session (PR-A) whose transcript still exists,
spawn now boots the type's CLI with '<resume_arg> <uuid>' so the role comes
back into its prior conversational context instead of starting fresh; the
actas prompt is still passed on the resume branch, since resume restores
context only and the actas re-run re-establishes the watcher, the exclusivity
lock, and the active FROM (claim is idempotent for the same session id).

- claude-code manifest: add resume_arg=--resume.
- New scripts/drivers/types/claude-code/_transcript-exists.sh: the CLI-internal
  ~/.claude/projects/<munged>/<uuid>.jsonl layout (every char outside
  [A-Za-z0-9-] -> '-', verified empirically) stays in the driver, never core.
- spawn.sh: resolve resume_arg, add --fresh to force a fresh session, and gate
  resume on record-resolves AND transcript-exists (a stale record whose
  transcript was deleted falls back to fresh -- '--resume <gone>' errors out).

Verified before implementing (recorded in the PR): claude --resume <uuid> with
a positional prompt submits that prompt as the resumed session's first input
(print + real interactive tmux run), and the session uuid is stable across a
resume generation without --fork-session.

* feat(resurrect): tmux-resurrect post-restore seat assignment (#339)

After a tmux server restart, relaunch each role pane back into its session.
Wired as @resurrect-hook-post-restore-all (NOT @resurrect-processes -- a
verbatim argv re-run is wrong both ways: a fresh-boot argv starts another new
session, a name-resume argv stalls on the picker). resurrect restores panes as
plain shells; the hook resolves each role's CURRENT session at restore time and
send-keys the same resume-or-fresh command spawn builds.

- scripts/internal/resurrect-panes.sh: pure parse + command-construction core
  (agmsg_resurrect_plan, unit-tested against a fixture) + a live send-keys
  wrapper that only seats panes which restored as a shell. Matches a pane to a
  role by saved title or the '-n <name>' argv marker (whole name, never split).
- lib/boot-command.sh: factor the boot-command pieces out of spawn.sh --
  agmsg_actas_prompt / agmsg_role_resume_uuid / agmsg_role_cli_args -- so spawn
  and the resurrect hook build identical commands and cannot drift on flag order
  or the resume gate. spawn.sh now calls these (behavior unchanged; all spawn
  tests green). A design note records that a codex-style 'resume <id>' subcommand
  form would need a shape branch here (out of scope, follow-up).
- role-session record gains a type= field (the hook needs it to rebuild the boot
  command from the type manifest); actas-claim writes it; add agmsg_role_session_get.
- README: session-resume + tmux-resurrect setup (the two tmux.conf lines) and the
  fail-open / no-@resurrect-processes rationale.

The end-to-end kill-server/restore path is a manual checklist item in the PR.

* refactor(boot-command): one-key, cli-immediately-after resume convention (#339)

Prepare the resume path to serve both a FLAG-shaped resume (claude-code's
--resume <uuid>) and a SUBCOMMAND-shaped one (codex 0.142's resume <id>) from
a SINGLE manifest key. The resume_arg value is now emitted verbatim right after
the cli, immediately followed by the uuid -- mandatory position for a subcommand
(must lead the argv), harmless for a flag (order-independent), so one emission
order serves both with no per-shape branch and no second key.

- Split agmsg_role_cli_args: agmsg_role_resume_head emits '<resume_arg> <uuid>'
  (the cli-immediately-after head); agmsg_role_cli_args now emits only the
  name/prompt tail.
- spawn.sh and resurrect-panes.sh emit the head right after the cli, before
  model/spawn-options/name. claude behavior is unchanged (flags reorder freely);
  all spawn + resurrect tests stay green.
- New tests/test_boot_command.bats locks the convention (head composes adjacently
  after the cli; tail carries no resume token) + the actas-prompt / resume gate.

Replaces the earlier 'codex is a follow-up' design note with the one-key rule.

* feat(session-start): role-aware directive on resume (#339)

When a session's bare sid was recorded as a role's seat (by actas-claim) and
that (team, agent) is registered for this project, SessionStart now emits the
ROLE-FILTERED Monitor directive -- watch.sh with the 4th <agent> arg, which
restricts receive to that role AND re-claims its exclusivity lock -- plus a
role-aware note, instead of the generic unfiltered directive.

This covers a manual `claude --resume <uuid>` that bypasses spawn's actas boot
prompt: a resume restores context but not runtime state, so the resumed session
re-arms as its role automatically. Fail-open: no record, an unreadable one, or a
cross-project sid collision (the recorded (team, agent) is not one of this
project's registered pairs) falls through to today's generic directive
unchanged.

* feat(codex): resume a codex role into its thread (#339)

Extend session resume to codex, which resumes via a SUBCOMMAND
(`codex resume <SESSION_ID> [PROMPT]`, 0.142) rather than a flag. The one-key
cli-immediately-after convention already handles the shape: manifest
resume_arg=resume emits `codex resume <uuid>` right after the cli, then model/
prompt. Verified: codex resume loads prior context + submits the positional
prompt, accepts -m after the SESSION_ID, and errors on a gone uuid (so the
transcript pre-check is required).

- codex/_transcript-exists.sh: locate ~/.codex/sessions/**/rollout-*-<uuid>.jsonl
  (the uuid == session_meta payload.id == the resume SESSION_ID). Fail-open.
- codex/codex-record-session.sh: the codex-side record path (codex actas is
  send-side only and never runs actas-claim). Resolves the thread id preferring
  $CODEX_THREAD_ID; the newest-rollout-by-cwd fallback records ONLY when the
  match is unique -- concurrent codex sessions in one cwd are ambiguous, so it
  records nothing and the role boots fresh (a resume mis-fire is worse than fresh).
- codex/template.md: the actas flow now calls codex-record-session.sh, extending
  codex actas from send-side-only to also recording its resumable session.
- No name_arg for codex (no session display-name flag); tmux-resurrect title
  matching therefore does not apply to codex, but spawn/despawn resume does.
- README: session-resume now covers Claude Code + Codex.

Tests: codex transcript-exists, the record path (env / unique-fallback /
ambiguous-skip / no-match), and spawn emitting `codex resume <uuid>`.

* fix(codex): bash 3.2 compat + unset-HOME guard in codex-record-session (#339)

macOS ships bash 3.2, whose parser cannot handle a while loop containing $(...)
command substitutions when the whole loop is itself wrapped in an outer $(...)
capture -- it mis-tracks the nesting and errors ('syntax error near unexpected
token'). ubuntu's bash 5 parsed it fine, so only the macos bats leg failed.
Rewrite the matching-rollout scan to write ids to a temp file instead of an
outer tids=$(...) capture, keeping every $(...) un-nested (the same shape the
existing agmsg_resolve_codex_thread uses).

Also address a co1 nit: under 'set -u' an unset HOME made sessions_dir=$HOME/...
an unbound-variable abort rather than the intended silent no-op; use ${HOME:-}
and guard the dir check so it falls open to fresh.

* fix(spawn): suppress the rename tip for spawn-launched sessions (#339)

A dogfood run surfaced that the '/rename this session' tip fired even for
sessions launched via spawn, which are already named <team>-<agent> by name_arg
-- so the tip is redundant and its 'hand-started' premise is wrong.

spawn's boot script now exports AGMSG_SPAWNED=1 (type-independent, before the
CLI exec), and the claude-code actas flow only suggests /rename when
AGMSG_SPAWNED is unset (a hand-started session with no convention name). No
behavior change for hand-started sessions.

* fix(resurrect): set exec bit on directly-run #339 scripts (#339)

Dogfood found scripts/internal/resurrect-panes.sh committed 100644. tmux-
resurrect execs the @resurrect-hook-post-restore-all target DIRECTLY (not via
'bash <path>'), so without the exec bit the hook dies with Permission denied --
the layout restores but no pane gets seated (silent). codex-record-session.sh
had the same hole: the codex actas flow runs it as a bare path.

Set both to 100755 via update-index --chmod=+x, matching the sibling convention
(internal/init-db.sh, codex/codex-monitor.sh are 100755; sourced libs and the
_-prefixed driver hooks stay 100644). Add a test asserting resurrect-panes.sh is
executable (its scripts/internal/ dir is not force-chmod'd by the test harness,
so the check is meaningful).

* fix(resurrect): skip a role whose actas lock is held by a live session (#339)

Dogfood (kill-server -> restore) found a role pane that failed to reseat: its
role-session record had been sown from a session still LIVE in another process,
so the hook tried to resume that uuid, the CLI rejected the double-launch, and
the pane died back to a bare shell. The transcript-exists pre-check correctly
returned true (the transcript exists -- the live session is writing it), and the
'pane restored as a shell' guard can't tell that the role's owner is alive
elsewhere. The actas lock is the source of truth for that.

resurrect-panes.sh now mirrors spawn.sh's pre-flight: before seating a role it
checks actas_lock_state and skips when the lock is held by a live session
(other:*). A dead owner leaves a stale lock -> reported free -> the role is
reseated as before. Verified the app-dev1 artifacts: its record project matched
the transcript's on-disk location (worktree cwd), ruling out a cross-project-dir
transcript mismatch -- the lock was the sole cause.

* docs(session-resurrect): tmux-resurrect setup + resume reference (#339)

Per the requested structure: README keeps a minimal summary (sessions are named
<team>-<agent>, spawn resumes by default, tmux-resurrect one-liner) and links to
a new long-form docs/session-resurrect.md next to actas.md.

docs/session-resurrect.md covers: tmux-resurrect (+continuum) setup and the
@resurrect-hook-post-restore-all line (with the do-NOT-use-@resurrect-processes
warning); how records + the restore-time hook resolve each pane; full vs partial
restore; a table of what auto-restores / is skipped (live-held role) / stays a
bare shell / boots fresh; the manual claude --resume + actas fallback; and notes
on resume-restores-context-only, --fresh, and the untrusted-dir trust prompt.
English, neutral voice, generic examples (myteam-alice).

* fix(gemini): detect the real GEMINI_CLI env var, not GOOGLE_GEMINI_CLI (#351)

Gemini CLI exports GEMINI_CLI=1 in its shell tool environment; the
GOOGLE_GEMINI_CLI name in the manifest does not exist upstream, so gemini
sessions fell through detection (misdetected as codex in the report).
Keep GEMINI_API_KEY as the secondary signal.

Root cause identified by the reporter with upstream source references.

Closes #347

* fix(codex): bind the bridge to the role's recorded thread, not "loaded" (#350) (#353)

The codex bridge launched with --thread loaded, which resolves to whatever
conversation the app-server last touched. When a project cwd has run more than
one codex thread, "loaded" is ambiguous and a co-resident thread can capture a
role's messages -- they become turns on the wrong conversation, delivered
nowhere the operator sees, while still marked read (issue #350, confirmed from a
live bridge log: injected into a stale thread for many turns).

codex-bridge-launcher.sh now prefers this role's RECORDED codex thread (the
role-session record added in #339) when it exists AND is recorded for this
project, falling back to "loaded" only when there is no record (fail-open for
roles predating the record). A request-file thread still wins. Freshness holds
because a role re-runs actas on resume (#339), which rewrites the record.

The reuse check now also compares the bound thread (a new .thread state file
alongside .appserver), so a bridge first launched on "loaded" rebinds the moment
this role's record appears, instead of the app-server match alone keeping the
wrong-thread bridge alive.

Tests: launcher binds the recorded thread on a project match, falls back to
loaded with no record / a cross-project record, and writes the bound-thread file.

* Stop ancestor project resolution from over-reaching to $HOME / other teams (#357) (#359)

* fix(resolve-project): stop ancestor resolution from over-reaching to $HOME / other teams (#357)

The #92 ancestor walk resolves a session's project by climbing pwd's ancestors
to the nearest REGISTERED project. Two flaws let a stray registration capture
unrelated sessions (reported: a claude-code agent's project silently became
$HOME):

1. agmsg_registered_projects scanned ALL teams, so a poison registration in an
   unrelated team leaked into another team's resolution.
2. the walk would MATCH $HOME / '/', which are ancestors of nearly every dir --
   one registration there poisons every session beneath it.

(codex was unaffected: the poison was a claude-code registration, and the
registry scan is per-type.)

Fixes (fallbacks only -- the per-process marker, step 1, is untouched):
- Team-scoping: agmsg_resolve_project / _registered_projects / _ancestor_project /
  _gitcommon_project take an optional <team>; join.sh passes the join target team
  so its resolution only sees that team. Omitted team keeps the legacy all-teams
  scan (phased migration for team-agnostic callers: whoami/actas/watch/reset).
- Shallow-path exclusion: agmsg_find_registered_project_variant never returns '/'
  or $HOME (the walk may ascend through them, it just won't land on one).
- Source guard: join.sh refuses to register a project at '/' or $HOME.

Tests: poison $HOME/other-team/'/' registrations no longer capture resolution;
team scope returns only that team's projects (all-teams still works with no team);
join refuses $HOME and root. Existing resolve suite stays green.

* fix(resolve-project): normalize the shallow-path exclusion compare (#357 review)

koit review: the '/' and $HOME match exclusion did a raw string compare, so a
registration stored with a trailing/duplicate slash ('$HOME/', '//') would slip
past it -- the ancestor walk generates a trailing-slash candidate variant, so
such a poison IS matched, and the raw compare then failed to exclude it.

Normalize the matched value before comparing (agmsg_normalize_project_path
collapses duplicate slashes and strips a trailing one), so '$HOME/' and '//'
resolve to $HOME and '/' and are excluded. join.sh's guard already ran on the
normalized path; a trailing-slash $HOME still normalizes and is refused.

Tests: '$HOME/' and '//' poison registrations are still excluded; join refuses a
trailing-slash $HOME. (Normalization is logical, not symlink-resolving -- see the
PR notes on that boundary.)

* fix(resolve-project): drop the join $HOME/root registration guard (#357 review)

koit decision: registering a project AT $HOME or / is a legitimate use case --
both claude and codex support sessions whose cwd is $HOME -- so join must not
refuse or warn. The #357 protection stays entirely on the RESOLUTION side:

- team-scoping (fix 1) and the ancestor-landing exclusion (fix 2) remain.
- The exclusion means a $HOME registration only ever matches its EXACT path: a
  session started AT $HOME still resolves to $HOME (via the pwd fallback), while
  sessions in subdirectories beneath $HOME are NOT vacuumed up to it.

Remove the join.sh guard and its tests; add tests asserting join allows a $HOME
registration and that an exact-$HOME session still resolves $HOME. The
normalized-compare robustness (trailing slash / '//') stays on the exclusion
side only.

* fix(spawn): guard '/'-prefixed boot prompt against MSYS path conversion on Git Bash (#358)

MSYS rewrites a leading-slash argv token to a Windows path when a native
CLI is invoked: '/agmsg actas <name>' arrives as
'C:/Program Files/Git/agmsg actas <name>' and the spawned agent never
sees a valid skill invocation. Emit MSYS2_ARG_CONV_EXCL=/<cmd> on the
boot script's CLI launch line for '/'-prefixed types only: prefix-scoped
so genuine POSIX-path args keep converting, inert outside MSYS, and
'$'-prefixed boot scripts stay byte-identical (asserted by test).

Co-authored-by: Salmonellasarduri <ukkin1129@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(spawn): wrap boot script with bash -l for psmux on Windows (#335) (#363)

* fix(spawn): explicit bash invocation for psmux on Windows (#335)

psmux (Windows tmux-compatible multiplexer) hands the split-window /
new-window command token to CreateProcess/ShellExecute. An extensionless
boot script has no file association, so Windows shows an "Open with"
dialog instead of executing it — the agent never starts.

PR #329 gated the .command rename to Darwin (#282) but assumed tmux
would honor the shebang ("runs it via its shebang (tmux)"). This holds
for Unix tmux but not psmux: the root cause was left unaddressed and the
symptom changed from Notepad to the "Open with" dialog.

Prefix `bash -l` on Windows (MINGW/MSYS/CYGWIN) in launch_in_tmux(),
matching launch_windows_terminal's existing `wt.exe new-tab bash -l`
pattern. Applied to both new-window and split-window branches.
macOS/Linux unchanged.

Tested on psmux v3.3.4 + Windows 11:
- codex spawn (default-shell=bash): OK
- codex spawn (default-shell=pwsh): OK
- claude-code spawn (default-shell=bash): OK

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert "fix(spawn): explicit bash invocation for psmux on Windows (#335)"

This reverts commit 333a35957d1c474e9b94d2c63a7102ca197c03f7.

* fix(windows): Codex phantom DB書き込みを防止 — SKILL.mdにbash実行指示追加・writable_rootsパス形式修正

Windows環境でspawnされたCodexエージェントが正しいskill DBに書き込めず、
phantom DB (C:\c\Users\...) やワークスペース内DBに誤って書き込む問題を修正。

根本原因:
1. SKILL.mdにシェル指定がなく、Codex LLMがPowerShellで直接実行を試みる
   → MSYS自動パス変換が効かず /c/Users/... が C:\c\Users\... に解決
2. install.shのconfigure_codex_sandbox()がMSYS形式パスをwritable_rootsに
   書き込み、Codex Rustが正しくパスを解決できずsandboxがブロック

修正:
- SKILL.md: bash実行指示とAGMSG_STORAGE_PATHオーバーライド禁止を追記
- install.sh: cygpath -mでWindows mixed形式に変換してからwritable_rootsに書き込み

検証: 3体同時Codex spawn × 2ラウンド、計17メッセージ全てreal DBに到達、
phantom/local DB流出ゼロ

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(spawn): wrap boot script with bash -l for psmux on Windows (#335)

psmux launches processes via Windows APIs that do not process shebang
lines. An extensionless boot script passed directly to tmux
new-window/split-window is accepted but silently never executed.

Wrap with `bash -l` on MINGW/MSYS/CYGWIN — the same pattern
launch_windows_terminal already uses for wt.exe. Non-Windows platforms
are unaffected (the array defaults to the bare script path).

Tested on psmux 3.3.4 / MINGW64 / Windows 11: direct → FAIL,
bash -l → PASS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(spawn): cover the Windows tmux bash -l wrap (#335)

Two bats tests that stub uname and tmux to exercise launch_in_tmux's Windows
path: both new-window and split-window launch through bash -l on
MINGW/MSYS/CYGWIN, and stay on the bare boot path elsewhere.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: fujibee <fujibee@gmail.com>

* release: 1.1.7 (#376)

* release: 1.1.7

* chore(release): map non-conventional squash subjects into the 1.1.7 changelog

Two squash merges landed with bare PR titles (no feat:/fix: prefix), so
git-cliff dropped the headline feature (#344) and a fix (#359) from the
generated notes. Extend the existing non-conventional-subject parser
section so both appear in CHANGELOG.md and the tag-time release notes.

* fix(app-release): Authenticode-sign Windows binaries during tauri build (#354)

* fix(app-release): Authenticode-sign windows binaries during tauri build

Post-build in-place signing (azure/artifact-signing-action) mutated the
exe/msi after the updater's minisign .sig had been generated, so every
0.1.4 in-app update failed signature verification, and the in-place pass
corrupted the MSI's embedded cabinet (#333).

Sign during bundling instead: bundle.windows.signCommand invokes
app/scripts/sign-windows.ps1 (TrustedSigning PowerShell module,
DefaultAzureCredential via azure/login OIDC — trusted-signing-cli is not
usable here because it requires a client secret and the federated app
registration has none). The app exe is now signed before cab packing and
the .sig is computed over the final signed bytes. A post-build step
verifies shipped msi/exe are Authenticode-signed so a silent signCommand
regression cannot ship.

* fix(app-release): make the no-post-build-signing regression test actually catch regressions

co1 review on #354: tests/test_app_release_signing.bats's first test chained
two bare `! grep -q ...` statements. bash's `set -e` (which bats enables
for the test body) explicitly exempts `!`-negated commands from aborting on
failure, so the first assertion's failure was silently swallowed — only the
last statement's exit code decided the test's outcome. That's also why CI
stayed green despite the workflow's own explanatory comment containing the
literal string "artifact-signing-action", which the first assertion was
supposed to reject.

Fix: use `run` + explicit `[ "$status" -ne 0 ]` for each check (matches
this repo's other bats files, which always place a bare negated grep as a
test's LAST statement rather than chaining several). Also scope the
assertion to the actual `uses: azure/artifact-signing-action` step wiring
instead of the bare identifier, and reword the explanatory comment so it no
longer echoes the old action's name — belt and suspenders against the same
prose/assertion collision recurring.

* feat(app): 0.1.5 UI polish — sidebar collapse, chat pane min/max, Team Room toggle, About version, lucide icons (#377)

* feat(app): 0.1.5 UI polish — sidebar collapse, chat pane min/max, About version, lucide icons

Three 0.1.5 backlog items bundled into one PR (koit: these are all
minor display-type changes, ship together):

- Sidebar (team pane) collapses to a 44px icon-only rail so panes get
  more width. Rail: agmsg mark (click to expand) → + (new team/agent,
  same menu as full view) → team icon (click opens a team-switch
  popup, replacing the <select>) → spacer → app-user avatar (also
  expands) + a direct settings button. Spawning/messaging isn't
  offered from the collapsed rail at all (koit: acceptable trade-off).
  The collapse toggle sits in its own slim strip level with the
  overlaid macOS traffic lights when expanded; the collapsed state
  has no separate toggle button there since it would overlap the
  lights at 44px wide — expanding is via the mark/avatar instead.

- App-user chat pane gets Windows-style minimize/maximize header
  buttons (3 states: normal / minimized — history collapses away,
  leaving just the composer row, fixing the "my own messages show
  twice" fatigue real users reported once the room already shows
  everything / maximized — fills the whole content area, hiding the
  team room and agent panes for a focused 1:1 view). Session-only
  state, no persistence.

- About dialog now shows both the app version and the bundled
  agmsg-core version (e.g. "0.1.4 (core 1.1.6)") — previously just
  "Name: agmsg" with no version at all on Windows. A single combined
  string in AboutMetadata.version (not muda's separate version/
  short_version fields, which map to different, platform-divergent
  About-panel slots) so both platforms show identical text. The
  bundled-core reader is a new agmsg::pinned_core_version() helper,
  shared with agmsg_core_version_status's "pinned" field so the two
  can never disagree. (About dialog's ICON stays the OS default on
  Windows regardless — muda 0.19.3 hardcodes MB_ICONINFORMATION /
  TD_INFORMATION_ICON there, ignoring AboutMetadata.icon entirely; not
  fixable from app code, confirmed by reading muda's Windows impl.)

- Introduces lucide-react for the new icons this round touches
  (sidebar collapse/team/settings, chat min/max/restore) rather than
  more ad-hoc unicode glyphs/hand-drawn SVGs (koit) — also swapped the
  pre-existing expanded-view settings gear for the same Settings icon
  so both sidebar states match. Broader replacement of older glyphs
  (pane/tab close ×, tab arrow ▸) is out of scope for this pass.

* feat(app): Show Team Room toggle in the View menu; fix chat-maximize/hide interaction

Show Team Room: companion to Show User Chat — toggles the team-room tab
itself (not just its content) via a new TeamRoomVisible state on the Rust
side, mirrored to the frontend the same way showUserChat already is.
Switching it off while the room tab is active falls back to whichever
pane tab is active, or an empty-state hint if none exist yet. Also fixes
both View-menu checkboxes going stale after a language switch (menu
rebuild previously hardcoded them back to checked regardless of actual
state) via try_state, since the very first menu-build call happens
before either state is registered.

Also folds in the co1 static-review fix for #356: maximizing the chat
pane hides tabs/stage based on chatPaneState alone, so turning off Show
User Chat while maximized left tabs/stage hidden too with no chat
controls left to recover from — gated the hidden condition on
showUserChat as well.

* fix(app): view-menu checkboxes now actually reflect Show Team Room / Show User Chat state

Two related bugs surfaced testing the new Show Team Room toggle:

1. The frontend's showTeamRoom/showUserChat state defaulted to true
   unconditionally with no way to query the real value on mount. Rust
   holds the only durable copy of these flags, so a webview remount that
   doesn't restart the Rust process (Vite HMR during tauri dev, or any
   future webview reload) would silently diverge — the frontend resets to
   true while Rust and the menu checkbox keep whatever was last set.
   Added a view_visibility query command the frontend now calls once on
   mount to seed its state from Rust instead of guessing.

2. Hiding Team Room or User Chat from the frontend (tab/chat-header
   right-click) never updated the native menu's checkbox. Root cause:
   Menu::get()/Submenu::get() only search a menu's own direct children,
   never nested submenus — since these checkboxes live inside the View
   submenu, app.menu().get(TEAM_ROOM_MENU_ID) can never find them, so
   set_checked() silently no-op'd. This was invisible when toggling from
   the native menu itself (macOS already flips the checkbox's own visual
   state on click, regardless of whether our code's call landed), but any
   external sync attempt failed outright. Now holds Arc-cloned handles to
   both CheckMenuItems in managed state (ViewMenuCheckboxes, rebuilt
   alongside the menu on every language switch) so every call site
   (on_menu_event and both set_*_visible commands) flips the actual item
   directly instead of re-searching a tree that doesn't contain it.

* chore(app): bump AGMSG_CORE_REF to v1.1.7 (#379)

* chore(app): bump version to 0.1.5 (#380)

* feat(app): show agent and team status (#385)

* feat(app): add bounded PTY detection tail

* feat(app): emit detected agent state

* feat(app): add agent status reducer

* feat(app): show member agent status

* feat(app): add multi-team status rail

* fix(app): match blocked-prompt patterns across wrapped lines

A narrow pane wraps a prompt like "Do you want to proceed?" across
two terminal lines; the tail buffer previously joined lines with
"\n", so the wrapped phrase never matched classify()'s substring
patterns and the pane stayed stuck showing stale status instead of
turning Blocked. Join with a space instead so a wrap can no longer
split a pattern apart.

* feat(app): show team names in the status rail, remove the redundant expanded select

The expanded status rail now labels each row with its team name
(previously dots only), making the existing <select> next to it
redundant, so it's removed. The collapsed rail already has its own
team switcher (the Users-icon popup), so nothing new is needed there.

* fix(app): make pane status detection actually track real terminal state

Several compounding bugs kept panes stuck on the wrong status (#385):

- ink lays out prompt/spinner text with CSI Cursor Forward and Cursor
  Horizontal Absolute (columns via "\x1b[nC"/"\x1b[nG") instead of
  literal spaces, so stripping those escapes glued words together
  ("Do you want to proceed?" arrived as "Doyouwanttoproceed?") and
  defeated every substring pattern. strip_ansi now tracks the current
  column and re-renders the gap as real spaces.
- The 20-frame recency window only split on '\n', but ink redraws the
  spinner/status line in place with a bare '\r'. Dozens of redraw
  frames collapsed into a single "line" that never aged out, so a
  resolved prompt or a finished spinner could keep matching long after
  the real screen moved past it. Now splits on '\r' too.
- "Output changed" was driven by a raw push counter, so a blinking
  cursor or other zero-width escape noise (no printable characters)
  kept it permanently true and the 3-tick idle debounce never got to
  fire. Now compares the derived tail text instead.
- That debounce was asymmetric: a static tail only special-cased state
  Working (debounce toward Idle) and Blocked (stay sticky), so once a
  pane settled into Idle, the very next tick reclassified the same
  frozen tail from scratch. If that snapshot happened to still contain
  a stale spinner glyph (a synchronized-output redraw stalled
  mid-animation), it flipped straight back to Working, re-debounced
  to Idle, and repeated forever. A static tail now holds whatever
  state it's already in.
- classify() checked braille spinner glyphs for every agent type, but
  Claude Code's actual spinner cycles through sparkle glyphs
  (✢✳✶✻✽), confirmed from a live capture; braille never matched a
  claude pane. Also removed the now-dead output_seq plumbing that
  the tail-based comparison replaced.

All patterns/fixtures in the added tests are taken verbatim from a
live capture rather than guessed.

* feat(app): show idle as green everywhere, drop the focus-dependent done state

koit's feedback: status should just reflect reality regardless of
whether the pane happens to be focused. Removes the "seen"/"done"
(unseen-completion) concept entirely — idle is now always the plain
green dot, not hidden (member rows) or teal only when unfocused (both
rows and the team rail). PaneStatus drops to just { state }, and the
focus-tracking machinery that fed it (paneIsFocused/focusPane/
activeRef/focusedPaneRef) is gone with it.

* feat(app): move member status dot before the name, stop pulsing the team dot

koit's feedback. The member row's checkbox and status dot now share
the same leading slot — checkbox in the team room tab (where members
are selectable), status dot everywhere else — instead of the dot
trailing the name. The team rail's working dot keeps its color but
drops the pulse animation; only the per-member dot still pulses.

* feat(app): generalize blocked-menu detection beyond enumerated prompt text

Three more real dialogs turned up panes that never went Blocked, each
missing a different way (#385):

- The plan-mode-exit conflict dialog has no "Do you want to proceed?"
  at all — just a numbered list and an "Enter to select" footer.
- The plan-review "Ready to code?" screen has neither that footer nor
  the usual phrase — only "Would you like to proceed?", which isn't
  in any pattern list and never will be for every prompt Anthropic
  ships.

Enumerating each prompt's own wording doesn't scale, so this ports
herdr's structural approach (src/detect/manifest.rs) instead of
chasing more literal phrases: is_horizontal_rule/prompt_box_body/
after_last_horizontal_rule scope classify() down to the currently
open bordered box (pure box-drawing-character geometry, no words),
and is_chevron_menu_line recognizes claude's "❯ 1." selector marker
the same way herdr uses codex's "›" glyph — an option is highlighted
regardless of what the menu says. herdr still backs this with ~82
literal per-agent phrase rules on top of the same structural regions,
so this doesn't replace COMMON_BLOCKED, just adds the same fallback
layer.

The chevron check needed one correction after a live false positive:
the composer shows "❯ <whatever you're typing>" the same shape as "❯
1. <option>", so typing a message briefly flipped a pane to Blocked.
Fixed by requiring digits + '.' right after the glyph — what every
real menu's numbering actually looks like, not just "some content."

* fix(app): correct codex's blocked patterns and add title-based detection

Cross-checked our CODEX_BLOCKED list against herdr's actual codex.toml
instead of leaving it as an untested guess: "Press enter to continue"
and "Do you trust the contents of this directory?" were never real
Codex CLI strings and never matched anything; added the verified
"[y/n]", "yes (y)", "enter to submit all", and "press enter to
confirm or esc to cancel".

Bigger finding: Codex reports its live/blocked state through the
window title (OSC 0/2), not the screen — herdr's cheapest signal for
it is a Braille spinner glyph in the title for Working and "Action
Required" for Blocked. strip_ansi now captures the last-seen OSC 0/2
title instead of discarding it like every other OSC, and
detection_tail appends it to the flattened text so classify()'s
existing substring patterns pick it up with no new code path. Also
dropped codex's bogus "esc to interrupt" working fallback (confirmed
absent from codex.toml) now that the real title-based signal exists,
and switched matching to case-insensitive (herdr normalizes both
sides too, and we don't actually know the exact on-screen casing for
several of these).

Verified live against a real codex pane: all ten Braille spinner
frames (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏) showed up in the captured title exactly as
herdr's manifest describes.

* feat(app): snap pane dividers to terminal cell units, herdr-style gaps (#390)

* feat(app): snap pane dividers to terminal cell units, herdr-style gaps

Divider drags now snap to whole terminal columns/rows instead of free
pixel positions — koit prefers the discrete, cell-aligned feel over a
continuous drag. TerminalPane reports its actual cell size (px per
col/row) on every fit; every pane shares the same fixed font today, so
any one of them is representative for snapping math regardless of
which two panes flank a given divider.

The gap between panes is now derived from that same cell size (half a
cell of padding per side, so two adjacent panes' padding sums to one
full cell at the shared seam) instead of a fixed 4px/2px value — closer
to herdr's look, and what koit was actually asking about when the old
fixed gap read as narrower than herdr's.

With the gap now wide enough to read clearly, the divider's own
mid-gap hairline was redundant chrome — hidden by default, shown only
on hover or while that SPECIFIC divider is being dragged (a class
toggled on the divider element itself, not `resizing-col`/`row` on
`body`, which is shared with the sidebar/chat dividers and would have
lit up every same-axis pane divider during an unrelated drag).

Also brightened the pane frame itself into its own `--pane-border`
variable (koit: the shared `--border` was too faint to read as a
visible box at rest) rather than changing `--border` globally, which
27 other rules also depend on.

* fix(app): key the drag-highlight off divider identity, not a DOM ref

co1 review (PR #390): the previous version toggled the highlight class
directly on the grabbed divider's DOM element. For a 2x2 aligned grid
that survives its own transpose, but a 3+ segment grid transposes into
a different divider shape (grid-segment keys -> single keys), so React
unmounts the original element mid-drag and the highlight silently
disappears.

dividerDragKey (now in paneTree.ts, pure and testable) computes a
divider's identity as [...basePath, ...segmentPath] (or path directly
for an already-single divider) — the same split node's path on both
sides of the transpose, per transposeGrid's own doc. App.tsx now
tracks the dragged divider by this key in state instead of a captured
element, so the highlight survives regardless of which divider ends
up rendering it.

* feat(app): persist UI settings across restarts (#391)

* feat(app): persist UI settings across restarts

Persist to localStorage: Show Team Room / Show User Chat toggles,
terminal font size (now adjustable from Settings), sidebar collapse
state, and window size/position/maximized state (via
tauri-plugin-window-state). Also adds a "don't show again" suppression
for the auto-triggered app-user prompt on team switch.

Currently selected team was already persisted (LAST_TEAM_KEY); no
change needed there. Pane split ratios are left for a follow-up.

* fix(app): restore view toggles via lazy init, not a racy setState effect

co1 review on #391: showTeamRoom/showUserChat started at `true` and were
corrected by a mount effect's setState — but a persist effect keyed on
the same state captured the initial-render's stale `true` first and
wrote it back to localStorage, corrupting the saved value on every
restart. Lazily restore both from localStorage instead, matching the
sidebarCollapsed/terminalFontSize pattern; the seeding effect now only
pushes the (already-correct) initial value into Rust.

Also validates the restored terminal font size against the Settings
modal's own range (8-24) rather than just `> 0`, so a corrupted/stale
localStorage value can't reach xterm or persist itself back unchecked.

* fix(app): re-report cell size after a live font-size change

co1 rebase-delta review on #391: the font-size effect calls fit.fit()
(which changes xterm's internal rows/cols) but never called onCellSize,
so #390's divider snap/gap sizing stayed pinned to whatever font was
active on the .term-pane container's last actual resize — the
ResizeObserver that normally drives onCellSize doesn't fire for a font
change, since the container's own offsetWidth/offsetHeight don't move.

* fix(bin): normalise Windows backslash paths before handing to bash/curl (#392)

os.tmpdir()/path.join() return backslash-separated paths on Windows.
bash.exe (Git Bash/MSYS2) reconstructs argv from the raw Windows
command-line string using its own escaping rules, which mangle a raw
backslash path into something that doesn't exist, so setup.sh fails
with "No such file or directory" even though the file is there.

Normalise the tempfile path to forward slashes before passing it to
curl -o and bash; fs.rmSync keeps using the native path since it's an
in-process Node call. No-op on POSIX (no backslashes to replace).

Root-caused and verified on real Windows/Git Bash by @masa6161 in
#262; carried forward per the team's external-PR-rebase-inhouse
convention since main has moved well past the reporter's original
v1.1.2-based branch.

Closes #262.

Co-authored-by: masa6161 <14049926+masa6161@users.noreply.github.com>

* feat(app): detect grok/grok-build agent status (#395)

* feat(app): detect grok/grok-build agent status

Adds "grok" and "grok-build" to classify()'s recognized agent types,
covering two of grok's blocked-dialog shapes (permission, ask-user
question) and its Working status line.

grok's footer hints need AND logic and bottom-two-line scoping that
plain OR-substring matching can't express (co1 review, #384/grok):
requiring only one of several hints anywhere in the 20-line window
risked matching ordinary scrollback that happens to mention the same
word. TailBuffer::detection_tail evaluates these against the real,
still-line-structured text before flattening loses that structure,
and bakes the result into sentinel markers that classify() treats as
literal patterns like any other agent's — the same architecture
already used for codex's OSC-title signal and claude's chevron-menu
marker.

Two corrections after live verification against a real grok pane
(the first-pass patterns were guessed from herdr's grok.toml plus
secondhand notes, not checked against actual output):

- The real permission-dialog footer says "Ctrl+o:always-approve", not
  "Ctrl+o:yolo" as first assumed — that wrong string meant the
  3-condition AND could never fire at all.
- Working detection originally required a Braille spinner glyph at
  the START of the same line as the "[stop]" chip (matching herdr's
  own anchor, to avoid misreading the startup splash — which also
  draws its logo out of Braille — as Working). A live capture showed
  why that's too strict here: grok's "thinking" animation redraws
  several spinner/counter elements in the same screen region every
  tick, and TailBuffer is a linear byte stream, not a real 2D screen
  grid, so those redraws interleave into the captured text instead of
  each overwriting the last — the glyph and "[stop]" frequently don't
  land on the same reconstructed line despite being adjacent on
  screen. That line-anchored check matched only ~10 of 124 ticks
  during a real "thinking" stretch; "[stop]" alone, unanchored,
  matched consistently across long runs in the same capture. The
  splash-only false positive the line-anchor was meant to prevent
  doesn't apply to a bare "[stop]" check, since the splash never
  contains that text at all.

Also extends strip_ansi to handle CSI "row;colH"/"f" (Cursor
Position) — grok's multi-panel layout jumps between screen regions
with this, not just the within-row 'G' form already handled for
claude/codex. A forward row jump becomes a newline (there's no real
screen buffer to overwrite, so that's the closest honest
representation); the column portion reuses 'G's existing logic.

Not covered: the option-select dialog (gutter + key + ●/○ marker on
one line, per herdr) — its exact on-screen format wasn't available to
verify against real output, and guessing risks the same class of bug
this whole change exists to fix.

* fix(app): handle backward cursor-position row jumps too

co1 review (PR #395): the previous version only inserted a separator
newline for FORWARD row jumps (target row > current row), leaving the
tracked row stale after a backward one. grok's real "thinking"
animation jumps back up to redraw an earlier panel constantly, so
after the first forward/backward round-trip the separator silently
stopped firing and later redraws of that panel glued back together —
exactly the "response… [stop]ccancl"-style gluing this whole change
exists to fix, just recurring after the first cycle instead of never
happening at all.

Any row change — either direction — now gets a single separator
newline and updates the tracked row; there's no real screen buffer to
overwrite either way, so that's the closest honest representation
regardless of direction or distance (previously: N newlines
proportional to a forward-only distance).

* fix(app): display chat timestamps in local time, not raw UTC (#394)

* fix(app): display chat timestamps in local time, not raw UTC

created_at is stored UTC (strftime('%Y-%m-%dT%H:%M:%SZ', 'now') in
init-db.sh); both display sites just sliced the raw string
(created_at.slice(11, 19)) with no timezone conversion at all.

Adds a timezone selector to the Settings modal (auto-detected via
Intl.DateTimeFormat().resolvedOptions().timeZone by default, manual
override via the full IANA list), persisted through the same
localStorage mechanism as the rest of Settings. Both timestamp call
sites now format through the selected zone.

Reported by otsune via X.

Closes #393

* fix(app): harden timezone resolution and cache formatters

Addresses co1's non-blocking nits on #394:
- Cache one Intl.DateTimeFormat per zone instead of constructing a new
  one per message per render.
- Validate a stored/selected zone via Intl before using it — an invalid
  value (corrupted localStorage, or a zone the runtime no longer
  recognizes) now falls back to the auto-detected zone instead of
  reaching formatMessageTime's own raw-UTC-slice fallback, which would
  silently reintroduce #393 for that one value. The persisted timezone
  state itself is sanitized the same way, so the Settings <select>
  always has a matching option.
- Add a midnight (00:00:00, not 24:00:00) regression test for the
  hour12:false guarantee.

* fix(app): pin the h23 hour cycle explicitly for message timestamps

* fix(app): replace the flat timezone select with a searchable combobox

* fix(app): resync auto-timezone draft text when the UI language changes

* fix(app): clean up the Settings font-size input (#401)

* fix(app): clean up the Settings font-size number input

* docs(app): fix stale keyboard-stepping claim in the spinner-hide comment

* perf(app): batch pty-output writes to one term.write() per animation frame (#402)

* perf(app): batch pty-output writes to one term.write() per animation frame

* fix(app): bound pty-output batching to a max latency and byte cap

* fix(app): make writeBatcher.dispose permanent and fix late-listener race

* feat(app): phase-lock agent-status and monitor pulse dots to wall clock (#403)

Both dot families now render through a shared PulseDot component that
samples a negative animation-delay from Date.now() at the exact moment
each dot's animation starts (mount, or the transition into "working"),
so every dot with the same period lands on the same frame regardless of
when it individually began pulsing.

* fix(codex-shim): forward args on a flags-only monitored launch (#404)

first_non_option() skips every -/--prefixed argument, so a flags-only
launch (e.g. `codex --yolo`, no subcommand) lands in the "" case of
the command_name dispatch. That was the only branch that didn't
forward "$@" to codex-monitor.sh, so the flags were silently dropped
and the session ran with default approval behavior instead of the
one requested.

The resume and * branches already forward the remaining arguments;
apply the same "$@" forwarding to the "" branch.

Root-caused, fixed, and verified end-to-end by @kimyuujin in #386.

Closes #386.

Co-authored-by: kimyuujin <238381601+kimyuujin@users.noreply.github.com>

* feat(app): green status-unknown default, roomier team-status-rail rows (#406)

Agent types classify() doesn't recognize land on "unknown", which
previously fell through to the same muted-gray base color as an actual
detection failure — give it green (same as idle) since it's just an
unhandled type, not an anomaly.

team-status-row switches from a fixed 20px height to member-row-style
padding, with the dot in its own centered slot (matching
member-check-slot), so team rows get breathing room and align the same
way agent-status dots do in the member list.

* feat(app): sidebar per-section + buttons, replacing the New dropdown (#407)

* feat(app): replace the sidebar's + New dropdown with per-section + buttons

koit's feedback: the expanded sidebar now has its own "Team" heading
(new, mirrors the existing "Agents" one) with a + button that opens
the new-team modal directly, and the "Agents" heading gets the same
treatment for spawning an agent — no more picking Team vs Agent from
a dropdown first. The collapsed rail keeps its dropdown as-is (no
room for two separate headers at 44px wide).

Member list's empty-state copy updated to match (pointed at the old
dropdown flow); all 9 locales translated.

* feat(app): close modals with Escape

Added to the shared Modal wrapper (modals.tsx) so every modal that
passes onClose gets it, not just the new-team/new-agent ones spawned
from the new sidebar + buttons.

* fix(app): don't close modals on Escape during IME composition

The Escape-to-close handler only checked e.key, so canceling an IME
conversion (kana->kanji, mid-composition) also closed the modal and
dropped whatever draft it held. Guards on isComposing, the WKWebView
keyCode-229 fallback, and defaultPrevented (so a child that already
consumed the key keeps the modal open). Pulled into a pure
shouldCloseOnEscape helper so the branching is unit-testable without
mounting a component.

* fix(antigravity): resolve Codex leftovers and delivery_modes mismatch (#408)

template.md had three copy-paste leftovers referring to Codex instead
of Antigravity (delivery-mode picker note, actas confirmation, mode-set
rejection message). Separately, type.conf declared
delivery_modes=monitor turn both off, but antigravity has no Monitor
tool or bridge equivalent (unlike codex/grok-build, which do) — the
template only ever offered turn/off and hard-rejected monitor/both.
Narrow type.conf to turn/off to match what's actually implemented.

Closes #399.

* fix(send): reject unregistered from/to agents (#409)

* fix(send): reject unregistered from/to agents

send.sh performed no existence check on either the from or to agent, so a
typo'd recipient (e.g. a stray send to "dummy") was accepted and stored:
the message landed addressed to nobody, was never deliverable, and
polluted team history with no signal at all (observed live: exit 0 on a
mistyped send). Validate both from and to against the team's roster
before inserting, listing the registered names in the error so a typo
is obvious. --force bypasses this for intentional pre-registration
sends. Validation lives in send.sh (the front door), not storage.sh, so
other entry points (api.sh) can keep their own policy.

Several existing tests sent through team/agent pairs that were never
joined (a shortcut that only worked because there was no validation);
updated those to join first, or pass --force where the test is
actually about concurrency/storage behavior rather than registration.

Closes #355.

* fix(send): document --force + roster validation, add regression tests

Address co1 review on #409:
- README.md / README.ja.md / SKILL.md described send.sh as taking
  exactly four positional arguments; document the optional trailing
  --force and the roster-validation contract.
- Add direct regression coverage for the new contract in
  test_messaging.bats: unregistered from/to are rejected without an
  insert, the error lists the registered roster, and --force bypasses
  the check even with no team config at all.

* fix(app): filter non-numeric characters out of the font-size draft (#405)

* fix(app): filter non-numeric characters out of the font-size draft

* fix(app): switch font-size input off type=number, fix IME + restore arrow-key stepping

* fix(app): don't hijack IME candidate navigation in font-size arrow-key stepping

* release: 1.1.8 (#410)

---------

Co-authored-by: fujibee <fujibee@gmail.com>
Co-authored-by: Salmonellasarduri <ukkin1129@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: masa6161 <14049926+masa6161@users.noreply.github.com>
Co-authored-by: kimyuujin <238381601+kimyuujin@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant