chopstick
All widgets

Commit & Push

Comes with Chopstick

Commits and pushes the changes in a git repo you have open (in an editor, terminal or agent), with your message or one the agent writes. With several repos open, pick which one in the card.

Already installed? It's in your widget library.

Type it in the notch's search

  • Commit and push message
  • Write a commit message

What it runs

A widget is a folder of plain files. Here is all of its code.

widget.json
{
  "id": "commit-push",
  "name": "Commit & Push",
  "icon": "icon.png",
  "description": "Commits and pushes the changes in a git repo you have open (in an editor, terminal or agent), with your message or one the agent writes. With several repos open, pick which one in the card.",
  "show_when": {
    "git.dirty": true,
    "app": {
      "matches": "^(com\\.t3tools\\.t3code|com\\.microsoft\\.VSCode|com\\.microsoft\\.VSCodeInsiders|com\\.todesktop\\.230313mzl4w4u92|com\\.exafunction\\.windsurf|dev\\.zed\\.Zed|com\\.apple\\.dt\\.Xcode|com\\.jetbrains\\..*|com\\.google\\.android\\.studio|com\\.sublimetext\\..*|com\\.panic\\.Nova|com\\.apple\\.Terminal|com\\.googlecode\\.iterm2|com\\.mitchellh\\.ghostty|dev\\.warp\\.Warp.*|com\\.github\\.wez\\.wezterm|net\\.kovidgoyal\\.kitty|org\\.alacritty|com\\.openai\\.codex|com\\.anthropic\\.claudefordesktop)$"
    }
  },
  "watch": [
    {
      "script": "repo.sh",
      "every": 5,
      "on": [
        "file:picked"
      ],
      "as": "git"
    }
  ],
  "initial": "ready",
  "states": {
    "confirm": {
      "task": true,
      "open": true,
      "compact": { "leading": { "symbol": "arrow.triangle.branch", "tint": "orange" }, "trailing": { "text": "{{git.name}}" } },
      "expanded": {
        "size": "regular",
        "header": { "symbol": "arrow.triangle.branch", "tint": "orange", "title": "Commit & Push" },
        "blocks": [
          { "stack": [ { "text": "{{input.message}}", "style": "title", "lines": 2 },
                       { "text": "{{git.name | No repo}} · {{git.branch}} · {{git.changed | 0}} changed · {{git.ahead | 0}} to push", "style": "caption" } ] },
          { "list": { "from": "git.files", "limit": 5, "title": "{{item.name}}", "symbol": "doc" } },
          { "input": { "placeholder": "Commit message…", "value": "{{input.message}}",
                       "do": [ { "set": { "message": "{{item.text}}", "repo": "{{git.path}}" } }, { "run": "commit.sh", "while": "pushing", "then": "done", "else": "failed" } ] } }
        ]
      },
      "keys": [ { "key": "esc", "do": [ "dismiss" ] } ]
    },
    "ready": {
      "compact": {
        "leading": {
          "symbol": "arrow.triangle.branch",
          "tint": "orange"
        },
        "trailing": {
          "text": "{{git.name}} · {{git.changed | 0}}"
        }
      },
      "expanded": {
        "size": "regular",
        "header": {
          "symbol": "arrow.triangle.branch",
          "tint": "orange",
          "title": "{{git.name | No repo}} · {{git.branch}}"
        },
        "blocks": [
          {
            "note": "{{git.changed | 0}} changed · {{git.ahead | 0}} to push"
          },
          {
            "list": {
              "from": "git.files",
              "limit": 5,
              "title": "{{item.name}}",
              "symbol": "doc"
            }
          },
          {
            "list": {
              "from": "git.others",
              "limit": 4,
              "title": "{{item.name}}",
              "subtitle": "{{item.changed}} changed · {{item.ahead}} to push",
              "symbol": "arrow.triangle.branch",
              "do": [
                {
                  "set": {
                    "pick": "{{item.path}}"
                  }
                },
                {
                  "run": "pick.sh"
                },
                {
                  "refresh": "git"
                }
              ]
            }
          },
          {
            "input": {
              "placeholder": "Commit message…",
              "do": [
                {
                  "set": {
                    "message": "{{item.text}}",
                    "repo": "{{git.path}}"
                  }
                },
                {
                  "run": "commit.sh",
                  "while": "pushing",
                  "then": "done",
                  "else": "failed"
                }
              ]
            }
          },
          {
            "row": [
              "spacer",
              {
                "button": {
                  "symbol": "sparkles",
                  "label": "Let {{agent_name | the agent}} write it",
                  "style": "plain",
                  "do": [
                    {
                      "set": {
                        "repo": "{{git.path}}"
                      }
                    },
                    {
                      "ask": "In the git repo at {{git.path}}, read the uncommitted changes (git status, git diff) and reply with only a one-line commit message, no quotes or other text. Don't commit anything.",
                      "stream_to": "agent",
                      "while": "drafting",
                      "then": "drafted",
                      "else": "failed"
                    }
                  ]
                }
              }
            ]
          }
        ]
      }
    },
    "drafting": {
      "compact": {
        "leading": {
          "symbol": "arrow.triangle.branch",
          "tint": "orange"
        },
        "trailing": {
          "chopsticks": {
            "tint": "{{agent_color}}"
          }
        }
      },
      "expanded": {
        "size": "regular",
        "header": {
          "symbol": "arrow.triangle.branch",
          "tint": "orange",
          "title": "{{git.name}} · {{git.branch}}"
        },
        "blocks": [
          {
            "row": [
              {
                "chopsticks": {
                  "tint": "{{agent_color}}"
                }
              },
              {
                "text": "{{agent.tool | Writing a message…}}",
                "style": "caption"
              }
            ],
            "align": "center",
            "spacing": "tight"
          }
        ]
      }
    },
    "drafted": {
      "task": true,
      "open": true,
      "on_enter": [
        {
          "set": {
            "message": "{{agent.reply}}"
          }
        }
      ],
      "compact": {
        "leading": {
          "symbol": "arrow.triangle.branch",
          "tint": "orange"
        },
        "trailing": {
          "text": "{{git.changed}} changed"
        }
      },
      "expanded": {
        "size": "regular",
        "header": {
          "symbol": "arrow.triangle.branch",
          "tint": "orange",
          "title": "{{git.name}} · {{git.branch}}"
        },
        "blocks": [
          {
            "text": "{{message}}",
            "style": "body",
            "lines": 3
          },
          {
            "row": [
              "spacer",
              {
                "button": {
                  "label": "Back",
                  "style": "plain",
                  "do": [
                    {
                      "go": "ready"
                    }
                  ]
                }
              },
              {
                "button": {
                  "symbol": "arrow.up.circle.fill",
                  "label": "Commit & Push",
                  "style": "filled",
                  "do": [
                    {
                      "run": "commit.sh",
                      "while": "pushing",
                      "then": "done",
                      "else": "failed"
                    }
                  ]
                }
              }
            ]
          }
        ]
      },
      "keys": [
        {
          "key": "return",
          "label": "Commit & Push",
          "do": [
            {
              "run": "commit.sh",
              "while": "pushing",
              "then": "done",
              "else": "failed"
            }
          ]
        }
      ]
    },
    "pushing": {
      "task": true,
      "compact": {
        "leading": {
          "symbol": "arrow.up.circle",
          "tint": "orange"
        },
        "trailing": "spinner"
      },
      "expanded": {
        "size": "regular",
        "blocks": [
          {
            "text": "Committing and pushing {{git.name}}…",
            "style": "caption"
          }
        ]
      }
    },
    "done": {
      "task": true,
      "dismiss_after": 4,
      "on_enter": [
        {
          "refresh": "git"
        }
      ],
      "compact": {
        "leading": {
          "symbol": "checkmark.circle.fill",
          "tint": "green"
        },
        "trailing": {
          "text": "Pushed {{result.sha}}"
        }
      },
      "expanded": {
        "size": "regular",
        "blocks": [
          {
            "text": "Pushed {{result.sha}} to {{git.branch}}",
            "style": "title"
          },
          {
            "note": "{{message}}"
          }
        ]
      }
    },
    "failed": {
      "task": true,
      "open": true,
      "compact": {
        "leading": {
          "symbol": "exclamationmark.triangle.fill",
          "tint": "red"
        },
        "trailing": {
          "text": "Failed"
        }
      },
      "expanded": {
        "size": "regular",
        "header": {
          "symbol": "exclamationmark.triangle.fill",
          "tint": "red",
          "title": "Commit & push failed"
        },
        "blocks": [
          {
            "code": "{{error | Something went wrong}}"
          },
          {
            "row": [
              "spacer",
              {
                "button": {
                  "label": "Back",
                  "do": [
                    {
                      "go": "ready"
                    }
                  ]
                }
              }
            ]
          }
        ]
      }
    }
  },
  "commands": {
    "commit": {
      "description": "Stage all changes in the current repo, commit with the given message and push.",
      "phrase": "Commit and push {message}", "preview": { "state": "confirm" },
      "inputs": {
        "message": {
          "type": "string",
          "description": "The commit message"
        }
      },
      "do": [
        {
          "set": {
            "message": "{{input.message}}",
            "repo": "{{git.path}}"
          }
        },
        {
          "run": "commit.sh",
          "while": "pushing",
          "then": "done",
          "else": "failed"
        }
      ]
    },
    "draft": {
      "description": "Have the agent write a commit message for the current repo's changes, to review before pushing.",
      "phrase": "Write a commit message",
      "do": [
        {
          "set": {
            "repo": "{{git.path}}"
          }
        },
        {
          "ask": "In the git repo at {{git.path}}, read the uncommitted changes (git status, git diff) and reply with only a one-line commit message, no quotes or other text. Don't commit anything.",
          "stream_to": "agent",
          "while": "drafting",
          "then": "drafted",
          "else": "failed"
        }
      ]
    }
  },
  "preview": {
    "states": [
      "ready",
      "drafting",
      "drafted",
      "pushing",
      "done",
      "failed"
    ],
    "data": {
      "git": {
        "path": "~/Projects/my-app",
        "name": "my-app",
        "branch": "main",
        "changed": 4,
        "ahead": 0,
        "dirty": true,
        "files": [
          {
            "name": "src/App.tsx"
          },
          {
            "name": "src/api/client.ts"
          },
          {
            "name": "README.md"
          },
          {
            "name": "package.json"
          }
        ],
        "count": 2,
        "others": [
          {
            "name": "website",
            "path": "~/Projects/website",
            "changed": 2,
            "ahead": 1
          }
        ]
      },
      "agent_name": "Claude Code",
      "agent": {
        "tool": "Bash: git diff",
        "reply": "Add suggestion drafts and settings for widget ideas"
      },
      "message": "Add suggestion drafts and settings for widget ideas",
      "result": {
        "sha": "a3f9c21"
      },
      "error": "! [rejected] main -> main (fetch first)\nhint: Updates were rejected because the remote contains work that you do not have locally."
    }
  }
}
commit.sh
# Stages everything in $CHOPSTICK_REPO, commits with $CHOPSTICK_MESSAGE and pushes.
set -e
[ -n "$CHOPSTICK_REPO" ] || { echo "No repo found" >&2; exit 1; }
[ -n "$CHOPSTICK_MESSAGE" ] || { echo "Write a commit message first" >&2; exit 1; }
cd "$CHOPSTICK_REPO"
git add -A
git diff --cached --quiet || git commit -q -m "$CHOPSTICK_MESSAGE" >&2
if git rev-parse --abbrev-ref @{u} >/dev/null 2>&1; then
  git push -q >&2
else
  git push -q -u origin HEAD >&2
fi
echo "{\"sha\": \"$(git rev-parse --short HEAD)\"}"
pick.sh
# Shows the repo at $CHOPSTICK_PICK in the card (kept while it has changes).
printf '%s' "$CHOPSTICK_PICK" > "$CHOPSTICK_STORAGE/picked"
echo '{}'
repo.sh
# Every git repo with something to commit or push that's open somewhere (a terminal, editor or
# agent working in it). The one shown is the one picked in the card, if it still has changes,
# else the one changed most recently; the rest are listed to switch to.
repos=$(mktemp)
trap 'rm -f "$repos"' EXIT
lsof -a -d cwd -u "$(id -un)" -Fn 2>/dev/null | sed -n 's|^n||p' | grep "^$HOME/" | grep -v "^$HOME/Library/" | sort -u \
  | while IFS= read -r dir; do git -C "$dir" rev-parse --show-toplevel 2>/dev/null; done | sort -u > "$repos"

esc() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }
picked=$(cat "$CHOPSTICK_STORAGE/picked" 2>/dev/null)
best=""; best_time=0; list=""
while IFS= read -r repo; do
  [ -n "$repo" ] || continue
  changed=$(git -C "$repo" status --porcelain 2>/dev/null | wc -l | tr -d ' ')
  ahead=$(git -C "$repo" rev-list --count @{u}..HEAD 2>/dev/null || echo 0)
  [ "$changed" -gt 0 ] || [ "$ahead" -gt 0 ] || continue
  # When it last changed: its newest changed file, else its last commit.
  t=$(cd "$repo" && git status --porcelain 2>/dev/null | cut -c4- | sed 's/.* -> //' | head -200 \
      | while IFS= read -r f; do stat -f %m "$f" 2>/dev/null; done | sort -n | tail -1)
  [ -n "$t" ] || t=$(stat -f %m "$repo/.git/index" 2>/dev/null || echo 0)
  if [ "$repo" = "$picked" ]; then t=9999999999; fi
  if [ "$t" -gt "$best_time" ]; then best_time=$t; best=$repo; fi
  list="$list$repo|$changed|$ahead
"
done < "$repos"

if [ -z "$best" ]; then echo '{"dirty": false, "changed": 0, "ahead": 0, "count": 0, "others": []}'; exit 0; fi
others=$(printf '%s' "$list" | while IFS='|' read -r repo changed ahead; do
  [ -n "$repo" ] && [ "$repo" != "$best" ] || continue
  printf '{"name": "%s", "path": "%s", "changed": %s, "ahead": %s},' "$(esc "$(basename "$repo")")" "$(esc "$repo")" "$changed" "$ahead"
done | sed 's/,$//')
count=$(printf '%s' "$list" | grep -c '|')
cd "$best"
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
changed=$(git status --porcelain | wc -l | tr -d ' ')
ahead=$(git rev-list --count @{u}..HEAD 2>/dev/null || echo 0)
files=$(git status --porcelain | head -8 | cut -c4- | sed 's/\\/\\\\/g; s/"/\\"/g; s/.*/{"name": "&"}/' | paste -sd, -)
printf '{"path": "%s", "name": "%s", "branch": "%s", "changed": %s, "ahead": %s, "dirty": true, "count": %s, "files": [%s], "others": [%s]}\n' \
  "$(esc "$best")" "$(esc "$(basename "$best")")" "$(esc "$branch")" "$changed" "$ahead" "$count" "$files" "$others"