SLOPSHOPPER

agent-progress

Shows the active agent run (scripts/agent) in the session: status line, pane and a toast per milestone, without a model turn

newpanecommandtoaststatusprocess
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · agent-progress
│ ┃ Agent run ✕ › fix the failing auth test and add an audit log call │ ┃ No agent run found. │ ⏺ Read(src/auth.ts) │ ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /agent-progress │ ⎿ agent-progress: Agent run pane opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Agent run
No agent run found.
README

TradingAgents Platform

CI

A web platform for running and managing TradingAgents analyses without modifying its code: start an analysis from the browser, watch the agents work live, then read the decision, the reports and the debates, and export them. The roadmap is the GitHub issues; docs/event-protocol.md describes the runner contract.

An analysis: agent pipeline and the final decision

ArtifactPathStack
Platform (backend + UI, one jar)artifact/backend, artifact/frontendJava 25, Spring Boot 4.1 · Vue 3.5, Vite, Naive UI
Runner (installed into the TradingAgents runtime)artifact/ta-runnerPython 3.12, uv, TradingAgents v0.5.1
Docker images and Compose setupdeploy/, artifact/ta-runner/DockerfileTemurin 25 JRE · PostgreSQL 18 · docker-socket-proxy
Server deploymentdeploy/server/Ansible over SSH
End-to-end tests (not shipped)e2e/Playwright, against the built jar

Two ways to run it: on your machine with mise run run (Quick start), or with Docker Compose, where each analysis runs in its own container and the data is kept in PostgreSQL (Run with Docker Compose).

Quick start

1. Prerequisites

  • mise — it installs the rest: Java 25 (Temurin) and uv, pinned in mise.toml, into its own directory. Install it with curl https://mise.run | sh (or brew install mise).
  • An API key for at least one LLM provider (OpenAI, Anthropic, Google, DeepSeek, xAI, Mistral, Groq, …), or a local Ollama, which needs no key.
  • Docker or Podman (mise run setup installs one when there is none: Docker on Ubuntu or Debian, Podman on macOS with Homebrew; PLATFORM_INSTALL_ENGINE=0 skips that). Under mise run, a running Podman machine's socket is set as DOCKER_HOST unless it is set already (scripts/container-env.sh); a Gradle daemon started before that needs ./gradlew --stop. mise run run and mise run dev start a local PostgreSQL 18 container (mise run db; its data is in the volume trading-analysis-platform-db; mise run db:stop stops it, mise run db:reset deletes its data). To use your own PostgreSQL 18 server instead, set PLATFORM_DB_HOST, PLATFORM_DB_PORT, PLATFORM_DB_NAME, PLATFORM_DB_USER and PLATFORM_DB_PASSWORD; then no container is started if PLATFORM_DB_HOST is another host. For a server on localhost also set PLATFORM_DB_CONTAINER=false, else the container is started and needs the port free (Docker is still needed for the backend tests).
  • Nothing else: Node.js and pnpm are downloaded by the build, Python 3.12 by uv.

2. Start the platform

git clone https://github.com/ramazangirgin/trading-analysis-platform.git
cd trading-analysis-platform
mise trust && mise install   # once: Java 25 and uv for this repository
mise run setup               # once: ta-runner (downloads TradingAgents), the UI packages, a container engine if missing
mise run run                 # builds the app if needed and serves it on :8080

Open http://127.0.0.1:8080. One process serves both the UI and the API; keep the terminal open while you use it, and stop it with Ctrl+C. The first start takes a few minutes (Gradle's dependencies, Node, building the UI and the backend); after that it starts in seconds, and after a git pull it rebuilds only what changed. Analyses, presets and keys live on disk (Where things live), so they survive restarts.

If the platform stops while an analysis runs (a crash, a kill), the analysis keeps going; the restarted platform follows it again, or records its result if it finished meanwhile, and starts queued analyses again. Ctrl+C in the terminal stops the running analyses too.

Changing the code instead? See Development — it runs the UI with hot reload.

Next: log in, add an API key and start an analysis.

Documentation

FileWhat it coversWho reads it
docs/user-guide.mdQuick start steps 3 to 8: log in and users, API keys, starting an analysis, watching and sharing it, past analyses, reports and export, languageUsers
docs/docker-compose.mdRunning the platform, PostgreSQL and the analyses as containers, data, security notes, upgrading between major versionsOperators
docs/configuration.mdWhere the platform keeps its data, and the LLM pricesUsers, operators
docs/login-and-backend.mdThe session login, password policy, lockout, public routes, users, roles, permissions and the endpoints behind themOperators, developers
docs/development.mdDev servers, the mise tasks, coding conventions, before pushing, ta-runner, testing the Docker runnerDevelopers
docs/ci-and-releases.mdThe CI jobs and the rules on main, dependency updates, setting up the repository on GitHub, versioning and releasesDevelopers
docs/agentic-development.mdTaking an issue to a reviewed pull request with agentsDevelopers
docs/event-protocol.mdThe contract between the backend and ta-runnerDevelopers
docs/coding-convention/README.mdWhere each kind of class, component or module belongs, and the rules the build checksDevelopers
deploy/server/README.mdSetting up an Ubuntu server over SSH and deploying the Compose stack on itOperators

Agentic development

An issue can be taken to a reviewed pull request by agents, with you deciding at two points: approving the plan and merging the pull request. Everything runs on your machine with Claude Code and your own Claude Code login: Sonnet as the developer agent, Opus as the review agent. Agents never merge. Each step, what to check at each decision, how to stop, continue or redo a run, and the settings: docs/agentic-development.md.

License

Licensed under the Apache License 2.0. Copyright 2026 Ramazan Girgin.

TradingAgents, which ta-runner installs, is also Apache-2.0-licensed and remains under its authors' copyright.

Source 2 files
hooks/register.tsx 136 lines
1import { atom, read, update } from 'claude-code'
2import type { Hook, Register } from 'claude-code'
3
4import type { RunView } from '../types'
5
6const PANE = 'agent-progress'
7const POLL_MS = 3000
8// Milestones that say where the loop is going, not what happened.
9const QUIET = /^#\d+: (next step|waiting for)/
10const run = atom({ plugin: 'agent-progress', key: 'run' } as const, null)
11
12type Event = { at: number; kind: string; parent: string; name: string; text: string }
13
14const parse = (tsv: string): Event[] =>
15  tsv
16    .split('\n')
17    .filter(Boolean)
18    .map(line => {
19      const [at, kind, parent, name, ...text] = line.split('\t')
20      return { at: Number(at), kind, parent, name, text: text.join('\t') }
21    })
22
23const summarise = (name: string, events: Event[], isActive: boolean): RunView => {
24  const steps: Event[] = []
25  const subSteps: Event[] = []
26  let tokens = 0
27  for (const e of events) {
28    if (e.kind === 'begin' && e.parent === '') steps.push(e)
29    if (e.kind === 'end' && e.parent === '') {
30      const at = steps.findLastIndex(s => s.name === e.name)
31      if (at >= 0) steps.splice(at, 1)
32    }
33    if (e.kind === 'begin' && e.parent !== '') subSteps.push(e)
34    if (e.kind === 'end' && e.parent !== '') {
35      const at = subSteps.findLastIndex(s => s.name === e.name && s.parent === e.parent)
36      if (at >= 0) subSteps.splice(at, 1)
37    }
38    if (e.kind === 'usage') tokens += Number(/tokens=(\d+)/.exec(e.text)?.[1] ?? 0)
39  }
40  const step = steps.at(-1)
41  const subStep = subSteps.findLast(s => s.parent === step?.name)
42  return {
43    name,
44    isActive,
45    step: step?.name ?? '',
46    subStep: subStep?.name ?? '',
47    stepSince: step?.at ?? 0,
48    tokens,
49    milestones: events
50      .filter(e => e.kind === 'milestone' && !QUIET.test(e.text))
51      .slice(-12)
52      .map(e => ({ at: e.at, text: e.text })),
53  }
54}
55
56const tokensText = (n: number) =>
57  n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${Math.round(n / 1e3)}k` : `${n}`
58const minutes = (seconds: number) => `${Math.max(0, Math.round(seconds / 60))}m`
59const clock = (at: number) => new Date(at * 1000).toTimeString().slice(0, 5)
60
61type Dollar = Parameters<Hook<'session.start'>>[0]
62
63// The state of every run is in the main repository's .git, shared by the worktrees.
64let agentDir: string | undefined
65const seen = new Map<string, { mtimeMs: number; milestones: number }>()
66let newest = { mtimeMs: 0, name: '' }
67
68async function poll($: Dollar) {
69  agentDir ??= await $.process
70    .run(['git', 'rev-parse', '--path-format=absolute', '--git-common-dir'])
71    .then(r => (r.exitCode === 0 ? `${r.stdout.trim()}/agent` : ''))
72  if (!agentDir) return
73  const dirs = await $.fs.list(agentDir).catch(() => [])
74  for (const dir of dirs) {
75    const tsv = `${agentDir}/${dir.name}/progress.tsv`
76    const stat = await $.fs.stat(tsv).catch(() => undefined)
77    if (!stat) continue
78    if (stat.mtimeMs > newest.mtimeMs) newest = { mtimeMs: stat.mtimeMs, name: dir.name }
79    if (seen.get(dir.name)?.mtimeMs === stat.mtimeMs) continue
80    const events = parse(await $.fs.read(tsv))
81    const milestones = events.filter(e => e.kind === 'milestone' && !QUIET.test(e.text))
82    const before = seen.get(dir.name)
83    seen.set(dir.name, { mtimeMs: stat.mtimeMs, milestones: milestones.length })
84    // The first look at a run is its baseline: its history is not announced.
85    for (const m of before ? milestones.slice(before.milestones) : []) {
86      $.ui.toast(`${dir.name.split('-')[0]}: ${m.text}`)
87    }
88  }
89  if (!newest.name) return
90  const pid = await $.fs.read(`${agentDir}/${newest.name}/run.pid`).catch(() => '')
91  const isActive = pid.trim() !== '' && (await $.process.run(['kill', '-0', pid.trim()])).exitCode === 0
92  const view = summarise(newest.name, parse(await $.fs.read(`${agentDir}/${newest.name}/progress.tsv`)), isActive)
93  await update($, run, () => view)
94  if (!isActive) return $.ui.status(undefined)
95  const now = await $.clock.now()
96  const where = view.subStep ? `${view.step} ${view.subStep}` : view.step
97  $.ui.status(`#${view.name.split('-')[0]} ${where} · ${minutes(now / 1000 - view.stepSince)} · ${tokensText(view.tokens)} tok`)
98}
99
100export const register: Register = on => {
101  on('session.start', async ($, e, next) => {
102    await $.command.register({
103      name: 'agent-progress',
104      description: 'Show the active agent run in a pane',
105    })
106    $.clock.every(POLL_MS, () => poll($).catch(() => undefined))
107    return next(e)
108  })
109
110  on('command.run', { command: 'agent-progress' }, async $ => {
111    await $.ui.open({ id: PANE, title: 'Agent run' })
112    return { text: 'Agent run pane opened.' }
113  })
114
115  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
116    const { Box, Text } = $.ui.resolve(e)
117    const view: RunView | null = await read($, run)
118    if (!view) return <Text dimColor>No agent run found.</Text>
119    const room = Math.max(1, (e.viewport?.rows ?? 24) - 6)
120    return (
121      <Box flexDirection="column">
122        <Text bold>{view.name}</Text>
123        <Text dimColor={!view.isActive}>
124          {view.isActive ? 'running' : 'not running'} · {view.step || 'no step'}
125          {view.subStep ? ` · ${view.subStep}` : ''} · {tokensText(view.tokens)} tokens
126        </Text>
127        {view.milestones.slice(-room).map(m => (
128          <Text>
129            {clock(m.at)} {m.text}
130          </Text>
131        ))}
132      </Box>
133    )
134  })
135}
136
types/index.d.ts 16 lines
1export type RunView = {
2  name: string
3  isActive: boolean
4  step: string
5  subStep: string
6  stepSince: number
7  tokens: number
8  milestones: { at: number; text: string }[]
9}
10
11declare module 'claude-code' {
12  interface PluginState {
13    'agent-progress': { run: RunView | null }
14  }
15}
16