merge: 上流 v1.1.8 取り込み (#22 タスク1)#25
Merged
Merged
Conversation
…esurrect (fujibee#339) (fujibee#344) * feat(role-session): record (team, agent) -> session at actas-claim (fujibee#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 (fujibee#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 (fujibee#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 (fujibee#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 (fujibee#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 (fujibee#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 (fujibee#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 (fujibee#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 (fujibee#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 fujibee#339 scripts (fujibee#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 (fujibee#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 (fujibee#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).
fujibee#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 fujibee#347
…d" (fujibee#350) (fujibee#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 fujibee#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 fujibee#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 (fujibee#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.
…teams (fujibee#357) (fujibee#359) * fix(resolve-project): stop ancestor resolution from over-reaching to $HOME / other teams (fujibee#357) The fujibee#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 (fujibee#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 (fujibee#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 fujibee#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.
…on on Git Bash (fujibee#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>
…ee#335) (fujibee#363) * fix(spawn): explicit bash invocation for psmux on Windows (fujibee#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 fujibee#329 gated the .command rename to Darwin (fujibee#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 (fujibee#335)" This reverts commit 333a359. * 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 (fujibee#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 (fujibee#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 * 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 (fujibee#344) and a fix (fujibee#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.
…ld (fujibee#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 (fujibee#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 fujibee#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.
…m Room toggle, About version, lucide icons (fujibee#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 fujibee#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.
* 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 (fujibee#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 (fujibee#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.
fujibee#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 fujibee#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 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 fujibee#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 fujibee#391: the font-size effect calls fit.fit() (which changes xterm's internal rows/cols) but never called onCellSize, so fujibee#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.
…rl (fujibee#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 fujibee#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 fujibee#262. Co-authored-by: masa6161 <14049926+masa6161@users.noreply.github.com>
* 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, fujibee#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 fujibee#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).
…#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 fujibee#393 * fix(app): harden timezone resolution and cache formatters Addresses co1's non-blocking nits on fujibee#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 fujibee#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 number input * docs(app): fix stale keyboard-stepping claim in the spinner-hide comment
…frame (fujibee#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
…ck (fujibee#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.
…ee#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 fujibee#386. Closes fujibee#386. Co-authored-by: kimyuujin <238381601+kimyuujin@users.noreply.github.com>
fujibee#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.
…ujibee#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.
…fujibee#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 fujibee#399.
* 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 fujibee#355. * fix(send): document --force + roster validation, add regression tests Address co1 review on fujibee#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.
…ujibee#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
# Conflicts: # scripts/session-start.sh # scripts/spawn.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概要
Issue #22 タスク1: 上流 fujibee/agmsg を fork main に取り込む。
Issue 記載時点の対象は v1.1.7 だったが、着手時点で上流が v1.1.8 をリリース済みだったため、orange の承認のもと v1.1.8 まで一気に取り込んだ(意図は「最新リリースへの追従」)。
b01799c→ 上流v1.1.8(4b25697) をマージa613118(feat(remote): notification layer — event classes and subscriber routing (ADR 0006) #24 notification layer まで反映済みの origin/main)コンフリクト解消 (2 ファイル)
いずれも「同じ位置に双方が
source行を追加」した衝突で、両方残す解消:scripts/session-start.sh: fork のlib/manifest.sh+lib/gc.shと上流のlib/role-session.sh(Role-to-session affinity: named sessions, resume-by-role boot, tmux-resurrect integration fujibee/agmsg#339) を併存scripts/spawn.sh: fork のlib/manifest.shと上流のlib/role-session.sh+lib/boot-command.sh(Role-to-session affinity: named sessions, resume-by-role boot, tmux-resurrect integration fujibee/agmsg#339) を併存fork 独自機能の温存確認
scripts/remote.sh/scripts/lib/sync.sh: 上流に存在しないため fork 側そのままscripts/send.sh: バックグラウンド push (remote.sh push) 健在 + 上流 fix(send): reject unregistered from/to agents fujibee/agmsg#409 の from/to validation も取り込み済みscripts/check-inbox.sh/scripts/inbox.sh: pull フック +sync_mark_read健在scripts/watch.sh:remote_pull_interval健在tests/test_remote_sync.bats/docs/remote*.md: fork 側そのままテスト
ローカル (Windows / Git Bash) で
bats tests/フル実行: 700 件中 69 失敗。失敗は全て Windows 環境起因の既存問題で、マージ起因ではないことをベースライン比較で確認済み:origin/mainおよび上流純正v1.1.8で個別実行し、同一テストが同一の失敗をすることを確認bats (ubuntu-latest)/bats (macos-latest)に委ねる関連
🤖 Generated with Claude Code