SLOPSHOPPER

tmux-attention

Flags the tmux window hosting Claude with @claude ask or done

newguardpromptprocess
A shopper browsing a rack in a slop shop
README

Dotfiles

Managed using GNU Stow

Reminders

Structure: "name-of-package"/path-to/destination stow "package-to-install" to set up symlinks stow -D "package-to-uninstall" to remove symlinks

OMP on Omarchy

Install both packages with stow omp omarchy, then apply an Omarchy theme to generate the OMP palette. Restart OMP once after setup so it starts with the custom theme's file watcher. Subsequent Omarchy theme switches update OMP live. Changing the theme through Appearance settings alone does not start the watcher in the current OMP runtime.

Vivaldi on Omarchy

Install the packages and browser UI modification from the repository root as your desktop user:

make install-vivaldi

The target runs Stow, prompts for sudo to patch Vivaldi's system resources, and reapplies the current Omarchy theme. Stow alone is not sufficient. Restart Vivaldi once after it completes. Subsequent theme switches update its interface within about a second, without a debugging port or local server. The integration selects its own Omarchy theme and disables Vivaldi's theme scheduling.

Run make install-vivaldi again after Vivaldi updates, then restart the browser. The modification uses Vivaldi's internal API and may need changes when that API changes. The installation supports one desktop user and shares that user's palette with every Vivaldi profile using this installation.

To stop syncing, remove the omarchy-theme.js script tag from /opt/vivaldi/resources/vivaldi/window.html, restart Vivaldi, and select your preferred theme and scheduling settings.

Required tools

  • stow
  • make
  • kitty
  • tmux and TPM
  • starship
  • gh, with extensions dlvhdr/gh-dash and dlvhdr/gh-enhance
  • lazygit
  • lazydocker
  • fzf
  • ripgrep
  • zoxide
  • eza
  • uv
  • stylua
  • dcg, from github.com/Dicklesworthstone/destructive_command_guard, and jq
  • shellcheck and lua-language-server, installed by nvim through Mason
Source 1 files
hooks/register.ts 81 lines
1import type { EngineInterface, ProcessRunResult, Register } from 'claude-code'
2
3const DONE_AFTER_MS = 20_000
4
5let isFlagRaised = false
6const askingToolUseIds = new Set<string>()
7
8function tmux($: EngineInterface, args: readonly string[]): Promise<ProcessRunResult> {
9  return $.process.run(['tmux', ...args])
10}
11
12async function clearFlag($: EngineInterface): Promise<void> {
13  const pane = await $.env.get('TMUX_PANE')
14  if (pane === undefined || !isFlagRaised) return
15  isFlagRaised = false
16  await tmux($, ['set-option', '-w', '-t', pane, '-u', '@claude'])
17}
18
19async function raiseFlag($: EngineInterface, flag: 'ask' | 'done'): Promise<void> {
20  const pane = await $.env.get('TMUX_PANE')
21  if (pane === undefined) return
22
23  const shown = await tmux($, [
24    'display', '-p', '-t', pane, '-F',
25    '#{pane_active}\t#{window_active}\t#{session_attached}\t#{session_name}',
26  ])
27  if (shown.exitCode !== 0) return
28  const [paneActive, windowActive, sessionAttached, sessionName = ''] = shown.stdout.trim().split('\t')
29
30  const clients = await tmux($, ['list-clients', '-t', sessionName, '-F', '#{client_flags}'])
31  const isFocused = clients.stdout.split('\n').some(flags => flags.includes('focused'))
32  const isWatched = paneActive === '1' && windowActive === '1' && sessionAttached !== '0' && isFocused
33  if (isWatched) return
34
35  isFlagRaised = true
36  await tmux($, ['set-option', '-w', '-t', pane, '@claude', flag])
37}
38
39async function startAsking($: EngineInterface, toolUseId: string): Promise<void> {
40  if (askingToolUseIds.has(toolUseId)) return
41  askingToolUseIds.add(toolUseId)
42  await raiseFlag($, 'ask')
43}
44
45async function stopAsking($: EngineInterface, toolUseId: string): Promise<void> {
46  if (!askingToolUseIds.delete(toolUseId) || askingToolUseIds.size > 0) return
47  await clearFlag($)
48}
49
50export const register: Register = on => {
51  on('prompt.submit', async ($, e, next) => {
52    if (e.origin.kind === 'composer' || e.origin.kind === 'bridge') {
53      askingToolUseIds.clear()
54      await clearFlag($)
55    }
56    return next(e)
57  })
58
59  on('tool.call', async ($, e, next) => {
60    if (e.agentId === undefined && (e.tool === 'AskUserQuestion' || e.tool === 'ExitPlanMode')) await startAsking($, e.tool_use_id)
61    const result = await next(e)
62    if (!next.signal.aborted) await stopAsking($, e.tool_use_id)
63    return result
64  })
65
66  on('tool.check', async ($, e, next) => {
67    const verdict = await next(e)
68    if (verdict.decision === 'ask' && e.tool_use_id !== undefined) await startAsking($, e.tool_use_id)
69    return verdict
70  })
71
72  on('turn.complete', async ($, e, next) => {
73    if (e.agentId === undefined && askingToolUseIds.size > 0) {
74      askingToolUseIds.clear()
75      await clearFlag($)
76    }
77    if (e.agentId === undefined && !e.isAborted && e.durationMs >= DONE_AFTER_MS) await raiseFlag($, 'done')
78    return next(e)
79  })
80}
81