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

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.

| Artifact | Path | Stack |
|---|---|---|
| Platform (backend + UI, one jar) | artifact/backend, artifact/frontend | Java 25, Spring Boot 4.1 · Vue 3.5, Vite, Naive UI |
| Runner (installed into the TradingAgents runtime) | artifact/ta-runner | Python 3.12, uv, TradingAgents v0.5.1 |
| Docker images and Compose setup | deploy/, artifact/ta-runner/Dockerfile | Temurin 25 JRE · PostgreSQL 18 · docker-socket-proxy |
| Server deployment | deploy/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).
mise.toml, into its own directory. Install it with curl https://mise.run | sh (or brew install mise).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).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.
| File | What it covers | Who reads it |
|---|---|---|
| docs/user-guide.md | Quick start steps 3 to 8: log in and users, API keys, starting an analysis, watching and sharing it, past analyses, reports and export, language | Users |
| docs/docker-compose.md | Running the platform, PostgreSQL and the analyses as containers, data, security notes, upgrading between major versions | Operators |
| docs/configuration.md | Where the platform keeps its data, and the LLM prices | Users, operators |
| docs/login-and-backend.md | The session login, password policy, lockout, public routes, users, roles, permissions and the endpoints behind them | Operators, developers |
| docs/development.md | Dev servers, the mise tasks, coding conventions, before pushing, ta-runner, testing the Docker runner | Developers |
| docs/ci-and-releases.md | The CI jobs and the rules on main, dependency updates, setting up the repository on GitHub, versioning and releases | Developers |
| docs/agentic-development.md | Taking an issue to a reviewed pull request with agents | Developers |
| docs/event-protocol.md | The contract between the backend and ta-runner | Developers |
| docs/coding-convention/README.md | Where each kind of class, component or module belongs, and the rules the build checks | Developers |
| deploy/server/README.md | Setting up an Ubuntu server over SSH and deploying the Compose stack on it | Operators |
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.
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.
hooks/register.tsx 136 lines1import { 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}
136types/index.d.ts 16 lines1export 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