From adf62ac94854170294ac0f0b2ce13ddae0cd25c8 Mon Sep 17 00:00:00 2001 From: David Ibia Date: Sat, 10 Oct 2026 22:12:36 +0100 Subject: [PATCH] feat(owt): title workspace tabs and keep agents retitling them owt title names the tmux windows of the current workspace with one to three words and saves the title in the worktree's git dir so owt open reuses it. owt hook runs on omo's UserPromptSubmit and reminds the agent of the current title so it retitles the tab as the work changes. --- .config/omo/hooks.json | 3 +- .local/bin/owt | 98 ++++++++++++++++++++++++++++++++++++------ README.md | 5 ++- 3 files changed, 90 insertions(+), 16 deletions(-) diff --git a/.config/omo/hooks.json b/.config/omo/hooks.json index fa62164..efe015a 100644 --- a/.config/omo/hooks.json +++ b/.config/omo/hooks.json @@ -3,7 +3,8 @@ "UserPromptSubmit": [ { "hooks": [ - { "type": "command", "command": "~/.local/bin/omo-notify", "timeout": 10 } + { "type": "command", "command": "~/.local/bin/omo-notify", "timeout": 10 }, + { "type": "command", "command": "~/.local/bin/owt hook", "timeout": 5 } ] } ], diff --git a/.local/bin/owt b/.local/bin/owt index 5f040e6..072bba1 100755 --- a/.local/bin/owt +++ b/.local/bin/owt @@ -5,6 +5,7 @@ # with the agent launched inside it in its own tmux window. A workspace can # carry any number of Linear issues; the agent renames the branch to # feature/- once it knows them and keeps their status in sync. +# It also titles the window with one to three words naming the work. set -euo pipefail @@ -42,6 +43,12 @@ Usage (run from anywhere inside the repo): feature/- (e.g. feature/pra-107-108-imap-timeouts), moves each issue to In Progress, and to In Review with the PR link. owt open Open a tmux window running the agent in an existing workspace + owt title Title the tmux windows of the workspace you are in with one to + three words naming the work (owt title imap timeouts). The + agent does this itself; owt open reuses the saved title. + owt hook omo UserPromptSubmit hook (wired in ~/.omo/agent/hooks.json): + inside a workspace, tells the agent its tab title with each + prompt so it keeps the title current; elsewhere prints nothing owt ls List this repo's workspaces and their branches owt all List the workspaces of every repo with their branches and source checkouts (works from any directory) @@ -149,6 +156,27 @@ pr_info() { } ws_dir() { printf '%s/%s/%s\n' "$OWT_ROOT" "$(repo_name)" "$1"; } +# A workspace's tab title, kept in the worktree's own git dir so owt open can +# reuse it and removing the worktree removes it too. +title_file() { printf '%s/owt-title\n' "$(git -C "$1" rev-parse --path-format=absolute --git-dir)"; } + +ws_title() { + local file + file="$(title_file "$1")" + if [[ -f "$file" ]]; then cat "$file"; fi +} + +# tmux windows with a pane open in a workspace, one id per line. tmux reports +# resolved paths, so compare against the resolved workspace path. +ws_windows() { + local real_dir win path + command -v tmux >/dev/null && tmux list-panes -a >/dev/null 2>&1 || return 0 + real_dir="$(cd "$1" && pwd -P)" + tmux list-panes -a -F '#{window_id} #{pane_current_path}' | while read -r win path; do + if [[ "$path" == "$real_dir" || "$path" == "$real_dir"/* ]]; then printf '%s\n' "$win"; fi + done | sort -u +} + name_taken() { [[ -e "$(ws_dir "$1")" ]] || git -C "$(main_root)" show-ref --verify --quiet "refs/heads/feature/$1" } @@ -197,7 +225,8 @@ install_deps() { # Standing workspace conventions, appended to the agent's system prompt on every launch. workspace_prompt() { - local name="$1" dir="$2" branch + local name="$1" dir="$2" branch title + title="$(ws_title "$dir")" branch="$(git -C "$dir" symbolic-ref --quiet --short HEAD || printf 'a detached HEAD')" printf 'You are in owt workspace "%s" (%s), a git worktree of %s on branch %s. ' \ "$name" "$dir" "$(repo_name)" "$branch" @@ -207,17 +236,26 @@ workspace_prompt() { # shellcheck disable=SC2016 printf '%s is a placeholder: once you know the issues, and before the branch is pushed, rename it with `git branch -m feature/-`, where are the lowercased issue keys joined by hyphens with the team prefix written once (PRA-107 and PRA-108 give pra-107-108) and is a 2 to 5 word kebab-case summary. ' "$branch" fi + if [[ -n "$title" ]]; then + printf 'Its tmux tab is titled "%s". ' "$title" + else + printf 'Its tmux tab shows the workspace name until you title it. ' + fi + # shellcheck disable=SC2016 + printf 'As soon as you know what the work is, and again whenever its focus changes, run `owt title ` with one to three short words naming it, never a sentence (for example `owt title imap timeouts`). ' printf 'When a pull request is open, comment on each issue with a short summary and the PR link, and move each to In Review.' } # launch [initial message] launch() { - local name="$1" dir="$2" args + local name="$1" dir="$2" args window shift 2 args=(--name "$name" --append-system-prompt "$(workspace_prompt "$name" "$dir")" "$@") + window="$(ws_title "$dir")" + window="${window:-$name}" if [[ -n "${TMUX:-}" ]]; then - tmux new-window -n "$name" -c "$dir" -- "$OWT_AGENT" "${args[@]}" - say "opened tmux window '$name' running $OWT_AGENT" + tmux new-window -n "$window" -c "$dir" -- "$OWT_AGENT" "${args[@]}" + say "opened tmux window '$window' running $OWT_AGENT" else say "not in tmux; start with: cd $(printf '%q' "$dir") && $OWT_AGENT$(printf ' %q' "${args[@]}")" fi @@ -278,6 +316,45 @@ cmd_open() { launch "$name" "$dir" } +# Title the tmux windows of the current workspace with what is being worked on. +cmd_title() { + local name dir title words=() win renamed=0 + name="$(current_ws)" || die "not inside an owt workspace" + dir="$(ws_dir "$name")" + [[ -d "$dir" ]] || die "no workspace at $dir" + read -ra words <<<"$(printf '%s ' "$@" | tr -s '[:space:]' ' ' | tr -d '[:cntrl:]')" + [[ ${#words[@]} -ge 1 && ${#words[@]} -le 3 ]] \ + || die "a title is one to three words naming the work, e.g. owt title imap timeouts" + title="${words[*]}" + + printf '%s\n' "$title" >"$(title_file "$dir")" + while read -r win; do + tmux rename-window -t "$win" "$title" + renamed=$((renamed + 1)) + done < <(ws_windows "$dir") + say "titled '$name' as '$title' ($renamed tmux window(s) renamed)" +} + +# omo UserPromptSubmit hook. Inside a workspace it hands the agent the tab's +# current title with each prompt, so the title keeps up with the work. +cmd_hook() { + local name dir title context + [[ -t 0 ]] || cat >/dev/null + git rev-parse --git-dir >/dev/null 2>&1 || return 0 + name="$(current_ws)" || return 0 + dir="$(ws_dir "$name")" + [[ -d "$dir" ]] || return 0 + title="$(ws_title "$dir")" + if [[ -n "$title" ]]; then + context="This is owt workspace \"$name\" and its tmux tab is titled \"$title\". If this prompt changes what the work is about, or the focus shifts while you work, retitle it with \`owt title \`: one to three short words naming the work, never a sentence." + else + context="This is owt workspace \"$name\" and its tmux tab has no title yet, so it shows the workspace name. As soon as you know what the work is, title it with \`owt title \`: one to three short words naming the work, never a sentence (for example \`owt title imap timeouts\`). Retitle it whenever the focus shifts." + fi + context="${context//\\/\\\\}" + context="${context//\"/\\\"}" + printf '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"%s"}}\n' "$context" +} + cmd_ls() { local prefix prefix="$OWT_ROOT/$(repo_name)/" @@ -471,14 +548,8 @@ cmd_done() { fi # Windows with a pane open in the workspace, collected before the directory goes away. - # tmux reports resolved paths, so compare against the resolved workspace path. - local windows=() real_dir - real_dir="$(cd "$dir" && pwd -P)" - if command -v tmux >/dev/null && tmux list-panes -a >/dev/null 2>&1; then - while read -r win path; do - [[ "$path" == "$real_dir" || "$path" == "$real_dir"/* ]] && windows+=("$win") - done < <(tmux list-panes -a -F '#{window_id} #{pane_current_path}' | sort -u) - fi + local windows=() win + while read -r win; do windows+=("$win"); done < <(ws_windows "$dir") if [[ $merged == yes && $remote_branch == yes ]]; then git -C "$root" push --quiet origin --delete "$branch" && say "deleted remote branch $branch" @@ -494,7 +565,6 @@ cmd_done() { say "deleted local branch $branch" fi - local win for win in $(printf '%s\n' "${windows[@]+"${windows[@]}"}" | sort -u); do tmux kill-window -t "$win" 2>/dev/null || true done @@ -504,6 +574,8 @@ case "${1:-}" in new) shift; cmd_new "$@" ;; done) shift; cmd_done "$@" ;; open) shift; cmd_open "$@" ;; + title) shift; cmd_title "$@" ;; + hook) shift; cmd_hook ;; ls) shift; cmd_ls ;; all) shift; cmd_all ;; cd) shift; cmd_cd "$@" ;; diff --git a/README.md b/README.md index 7f10034..4225227 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ Key features: OpenCode is retired. Its agents, commands and plugins are left in `.config/opencode/` for reference, are no longer installed or linked, and its skills and slash commands moved here. - `omo.jsonc` (linked as `~/.omo/omo.jsonc`): model routing for the main session, task categories and curated agents. `design-critique-a` and `design-critique-b` are the two lanes the Impeccable critique delegates its assessments to. -- `hooks.json` (linked as `~/.omo/agent/hooks.json`): runs `omo-notify` when you send a prompt, when a session stops and when it asks you something, for the sound, the notification and the coloured tab marker. +- `hooks.json` (linked as `~/.omo/agent/hooks.json`): runs `omo-notify` when you send a prompt, when a session stops and when it asks you something, for the sound, the notification and the coloured tab marker. It also runs `owt hook` with each prompt, which tells an agent in an `owt` workspace its current tab title so it retitles the tab as the work changes. omo runs a new hook only after you approve it with `/hooks trust ` (ids are in `/hooks list`). - `prompts/` (linked as `~/.omo/agent/prompts`): slash commands. - `/linear-sync [notes]` posts a progress comment on the workspace's Linear issues and sets them to In Progress, or In Review once a PR is open. - `/owt-done [notes]` finishes a workspace after its PR merges: it comments on each issue and moves it to Done, then runs `owt done --detach`. It asks before discarding uncommitted or unmerged work. @@ -197,10 +197,11 @@ owt ls # workspaces and their current branches owt all # workspaces of every repo, with branches and source checkouts (from anywhere) owt cd # jump into a workspace with fzf; owt cd lagos goes straight there owt open lagos # reopen the agent in a workspace +owt title imap timeouts # title the workspace's tmux tab (one to three words); the agent does this itself owt done # remove the workspace you are in once its work is merged ``` -`owt new` starts from the latest `origin` default branch, copies local `.env` files, installs dependencies, and opens a tmux window running omo with the workspace conventions: rename the placeholder branch to `feature/-` before pushing, and keep the Linear issues' statuses in sync. Workspaces share the main checkout's omo memory through a link under `~/.omo/memory/agents/`. +`owt new` starts from the latest `origin` default branch, copies local `.env` files, installs dependencies, and opens a tmux window running omo with the workspace conventions: rename the placeholder branch to `feature/-` before pushing, title the tab with one to three words naming the work (`owt title`) and update it when the focus changes, and keep the Linear issues' statuses in sync. The title is saved with the worktree, so `owt open` reopens the tab under it. Workspaces share the main checkout's omo memory through a link under `~/.omo/memory/agents/`. `owt cd` works from any directory. Inside a repository the picker starts filtered to that repo's workspaces and jumps straight in when only one matches. The `owt` function in `.zshrc` performs the `cd`; without it, `owt cd` only prints the path.