feat: add experimental pushy delivery mode
Some checks failed
tamq-ci / test (push) Has been cancelled
Some checks failed
tamq-ci / test (push) Has been cancelled
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a03397-4d51-7fd1-8ff2-946eb22ea2bc
This commit is contained in:
parent
8186550c9a
commit
9ed8b62b45
16 changed files with 441 additions and 31 deletions
|
|
@ -34,7 +34,7 @@ from .terminal import format_comment
|
|||
SUBCOMMANDS = frozenset(
|
||||
"start attach serve stop status ping inbox history inspect ack send reply export replay purge completion db-version config tap".split()
|
||||
)
|
||||
START_OPTIONS = frozenset({"--command", "--cmd", "--tap", "--detach", "--no-service", "--no-display"})
|
||||
START_OPTIONS = frozenset({"--command", "--cmd", "--tap", "--detach", "--no-service", "--no-display", "--mode"})
|
||||
GLOBAL_FLAGS = frozenset({"--orwell", "--verbose"})
|
||||
|
||||
|
||||
|
|
@ -129,6 +129,10 @@ def ensure_output_service() -> bool:
|
|||
return ensure_service_capabilities({"manual_delivery", "terminal_output"})
|
||||
|
||||
|
||||
def ensure_pushy_service() -> bool:
|
||||
return ensure_service_capabilities({"manual_delivery", "pushy_input"})
|
||||
|
||||
|
||||
def preflight_runtime_paths() -> None:
|
||||
"""Ensure local state/runtime parents are available before tmux mutation."""
|
||||
parents = {db_path().parent, socket_path().parent, pid_path().parent, lock_path().parent}
|
||||
|
|
@ -154,12 +158,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
description="Repository-aware tmux sessions with durable local messaging.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""session shorthand:
|
||||
tamq [--detach] [--command COMMAND] [--no-display] REPO [REPO ...]
|
||||
tamq [--detach] [--command COMMAND] [--mode MODE] REPO [REPO ...]
|
||||
|
||||
With no --command, tamq opens ordinary repository shells. --command is an
|
||||
explicit initial command for newly created windows. Messages appear as
|
||||
sanitized terminal output by default; --no-display keeps them inbox-only.
|
||||
Neither mode injects message bytes into pane input.
|
||||
sanitized terminal output by default. --mode inbox keeps messages durable
|
||||
without display; experimental --mode pushy submits them to target input.
|
||||
|
||||
manual messaging from a managed shell:
|
||||
@TARGET: MESSAGE...
|
||||
|
|
@ -178,7 +182,8 @@ manual messaging from a managed shell:
|
|||
start.add_argument("repos", nargs="*", help="gita-registered repository slugs")
|
||||
start.add_argument("--command", "--cmd", dest="initial_command", default=None, help="explicit initial command for newly created windows (default: ordinary shell)")
|
||||
start.add_argument("--tap", action="store_true", help="explicitly opt in to PTY input observation and pane message injection")
|
||||
start.add_argument("--no-display", action="store_true", help="keep messages in the durable inbox without writing target terminal output")
|
||||
start.add_argument("--mode", choices=("output", "inbox", "pushy"), help="message delivery mode (default: output; pushy is experimental)")
|
||||
start.add_argument("--no-display", action="store_true", help="compatibility alias for --mode inbox")
|
||||
start.add_argument("--detach", action="store_true", help="detach after startup")
|
||||
start.add_argument("--no-service", action="store_true", help="open tmux windows without service registration or messaging")
|
||||
attach = subparsers.add_parser("attach", help="attach to the managed tmux session")
|
||||
|
|
@ -251,7 +256,7 @@ def normalize_argv(argv: list[str]) -> list[str]:
|
|||
if index == len(argv):
|
||||
return argv
|
||||
candidate = argv[index]
|
||||
if candidate in START_OPTIONS or (
|
||||
if candidate in START_OPTIONS or candidate.startswith("--mode=") or (
|
||||
not candidate.startswith("-") and candidate not in SUBCOMMANDS
|
||||
):
|
||||
return [*argv[:index], "start", *argv[index:]]
|
||||
|
|
@ -307,6 +312,15 @@ def main(argv: list[str] | None = None) -> int:
|
|||
finally:
|
||||
store.close()
|
||||
if args.command == "start":
|
||||
if args.no_display and args.mode not in (None, "inbox"):
|
||||
print("tamq: --no-display conflicts with the selected --mode", file=sys.stderr)
|
||||
return 2
|
||||
selected_mode = args.mode or ("inbox" if args.no_display else "output")
|
||||
endpoint_delivery_mode = {
|
||||
"inbox": "manual",
|
||||
"output": "output",
|
||||
"pushy": "pushy",
|
||||
}[selected_mode]
|
||||
if args.tap and args.initial_command is None:
|
||||
print("tamq: --tap requires an explicit --command", file=sys.stderr)
|
||||
return 2
|
||||
|
|
@ -316,6 +330,12 @@ def main(argv: list[str] | None = None) -> int:
|
|||
if args.tap and args.no_display:
|
||||
print("tamq: --tap cannot be combined with --no-display", file=sys.stderr)
|
||||
return 2
|
||||
if args.tap and args.mode is not None:
|
||||
print("tamq: --tap cannot be combined with --mode", file=sys.stderr)
|
||||
return 2
|
||||
if args.no_service and args.mode is not None:
|
||||
print("tamq: --mode cannot be combined with --no-service", file=sys.stderr)
|
||||
return 2
|
||||
manager = TmuxManager()
|
||||
try:
|
||||
if not args.no_service:
|
||||
|
|
@ -324,9 +344,14 @@ def main(argv: list[str] | None = None) -> int:
|
|||
except (TmuxError, OSError) as exc:
|
||||
print(f"tamq: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
service_ready = (
|
||||
ensure_manual_service() if args.no_display else ensure_output_service()
|
||||
) if not args.no_service else True
|
||||
if args.no_service:
|
||||
service_ready = True
|
||||
elif selected_mode == "inbox":
|
||||
service_ready = ensure_manual_service()
|
||||
elif selected_mode == "pushy":
|
||||
service_ready = ensure_pushy_service()
|
||||
else:
|
||||
service_ready = ensure_output_service()
|
||||
if not service_ready:
|
||||
print("tamq service failed to start with the required delivery capability; use --no-service to open repos without messaging", file=sys.stderr)
|
||||
return 1
|
||||
|
|
@ -339,7 +364,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
registered = False
|
||||
if not args.no_service:
|
||||
try:
|
||||
delivery_mode = "pane" if args.tap else ("manual" if args.no_display else "output")
|
||||
delivery_mode = "pane" if args.tap else endpoint_delivery_mode
|
||||
registration = asyncio.run(request({"op": "register", "endpoint_id": endpoint.endpoint_id, "instance_id": endpoint.instance_key, "pid": endpoint.pid, "session": endpoint.session, "repos": endpoint.repos, "delivery_mode": delivery_mode}))
|
||||
if not registration.get("ok"):
|
||||
raise RuntimeError(f"endpoint registration failed: {registration.get('error', 'unknown error')}")
|
||||
|
|
@ -367,7 +392,10 @@ def main(argv: list[str] | None = None) -> int:
|
|||
"delivered": delivered,
|
||||
"service": not args.no_service,
|
||||
"messaging": registered,
|
||||
"delivery_mode": "none" if args.no_service else ("pane" if args.tap else ("manual" if args.no_display else "output")),
|
||||
"mode": "none" if args.no_service else ("tap" if args.tap else selected_mode),
|
||||
"delivery_mode": "none" if args.no_service else (
|
||||
"pane" if args.tap else endpoint_delivery_mode
|
||||
),
|
||||
}
|
||||
print(json.dumps(summary), flush=True)
|
||||
if args.detach:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue