system-diff 0.4.0__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {system_diff-0.4.0 → system_diff-0.4.2}/PKG-INFO +5 -1
- {system_diff-0.4.0 → system_diff-0.4.2}/README.md +4 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/mods/system-diff/.claude-plugin/plugin.json +1 -1
- {system_diff-0.4.0 → system_diff-0.4.2}/mods/system-diff/hooks/page.tsx +7 -4
- {system_diff-0.4.0 → system_diff-0.4.2}/mods/system-diff/hooks/pane.test.tsx +12 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/mods/system-diff/hooks/register.tsx +19 -3
- {system_diff-0.4.0 → system_diff-0.4.2}/mods/system-diff/types/index.d.ts +2 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/pyproject.toml +1 -1
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/__init__.py +1 -1
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/agents.py +58 -3
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/canvas.py +17 -7
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/cli.py +2 -1
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/pane.py +14 -2
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/panel.py +3 -2
- {system_diff-0.4.0 → system_diff-0.4.2}/tests/test_design_and_pane.py +49 -1
- {system_diff-0.4.0 → system_diff-0.4.2}/.claude-plugin/marketplace.json +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/.gitattributes +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/.github/workflows/ci.yml +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/.github/workflows/release.yml +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/.github/workflows/system-diff.yml +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/.gitignore +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/LICENSE +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/docs/diagrams/system.json +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/docs/diagrams/system.svg +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/mods/system-diff/hooks/hooks.json +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/__main__.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/activity.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/agent.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/architecture.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/companion.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/compare.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/config.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/design.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/diff.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/extract.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/graph.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/inside.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/learn.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/mcp_server.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/narrate.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/pictures.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/png.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/profile.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/readme.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/sources.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/statusline.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/textmap.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/topics.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/tui.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/src/system_diff/watch.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/system-diff.toml +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/tests/conftest.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/tests/test_capture.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/tests/test_checkpoint_watch_mcp.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/tests/test_system_diff.py +0 -0
- {system_diff-0.4.0 → system_diff-0.4.2}/tests/test_terminal_and_learning.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: system-diff
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Draws your system from its code (parts, entry points, data, outside systems) and what each change adds, changes or removes. No dependencies; SVG pictures for PRs and READMEs.
|
|
5
5
|
Project-URL: Homepage, https://github.com/simplifiedmachine/system-diff
|
|
6
6
|
Project-URL: Documentation, https://github.com/simplifiedmachine/system-diff/wiki
|
|
@@ -92,6 +92,8 @@ Claude is changing in it. `/system-diff close` (or "× close" in its tab row) hi
|
|
|
92
92
|
- **Zoom:** Enter (or a second click) zooms in on a container's components, with the lines between them. `b` goes
|
|
93
93
|
back, `f` follows Claude again.
|
|
94
94
|
- **Scroll:** `[` `]` scroll a wide diagram across; shift ↑ ↓, Page Up/Down or the wheel scroll a tall one.
|
|
95
|
+
- **Zoom:** ⊖ and ⊕ in the tab row (or `-` and `=`) draw the diagrams compact (narrower boxes, no flow labels) or
|
|
96
|
+
tiny (titles only), to fit more of the system in a narrow pane; below 110 columns it is compact by itself.
|
|
95
97
|
- **This turn**: each change to the design, and what it trades off.
|
|
96
98
|
- **Learn**: how the part Claude works on is designed.
|
|
97
99
|
- The principle its place calls for: an edge of the system, an adapter, a shared foundation, a coordinator.
|
|
@@ -134,6 +136,8 @@ Claude does most work alone unless asked otherwise, so system-diff asks it to pl
|
|
|
134
136
|
nobody waits on anybody.
|
|
135
137
|
- **Claims:** `claim` gives an agent its blocks. An edit to someone else's block is published to its owner and shown
|
|
136
138
|
under Needs your attention.
|
|
139
|
+
- **One agent, one area:** when an agent (the main conversation included) edits blocks in two or more areas in one
|
|
140
|
+
turn, the edit hook tells it, once that turn, to plan a team with `plan_agents` and hand the rest to agents.
|
|
137
141
|
|
|
138
142
|
The MCP tools: `plan_agents`, `claim`, `publish`, `subscribe`, `inbox`, `team`. It all stays in `.git/system-diff/`
|
|
139
143
|
(`agents.json`, `bus.jsonl`), never pushed.
|
|
@@ -60,6 +60,8 @@ Claude is changing in it. `/system-diff close` (or "× close" in its tab row) hi
|
|
|
60
60
|
- **Zoom:** Enter (or a second click) zooms in on a container's components, with the lines between them. `b` goes
|
|
61
61
|
back, `f` follows Claude again.
|
|
62
62
|
- **Scroll:** `[` `]` scroll a wide diagram across; shift ↑ ↓, Page Up/Down or the wheel scroll a tall one.
|
|
63
|
+
- **Zoom:** ⊖ and ⊕ in the tab row (or `-` and `=`) draw the diagrams compact (narrower boxes, no flow labels) or
|
|
64
|
+
tiny (titles only), to fit more of the system in a narrow pane; below 110 columns it is compact by itself.
|
|
63
65
|
- **This turn**: each change to the design, and what it trades off.
|
|
64
66
|
- **Learn**: how the part Claude works on is designed.
|
|
65
67
|
- The principle its place calls for: an edge of the system, an adapter, a shared foundation, a coordinator.
|
|
@@ -102,6 +104,8 @@ Claude does most work alone unless asked otherwise, so system-diff asks it to pl
|
|
|
102
104
|
nobody waits on anybody.
|
|
103
105
|
- **Claims:** `claim` gives an agent its blocks. An edit to someone else's block is published to its owner and shown
|
|
104
106
|
under Needs your attention.
|
|
107
|
+
- **One agent, one area:** when an agent (the main conversation included) edits blocks in two or more areas in one
|
|
108
|
+
turn, the edit hook tells it, once that turn, to plan a team with `plan_agents` and hand the rest to agents.
|
|
105
109
|
|
|
106
110
|
The MCP tools: `plan_agents`, `claim`, `publish`, `subscribe`, `inbox`, `team`. It all stays in `.git/system-diff/`
|
|
107
111
|
(`agents.json`, `bus.jsonl`), never pushed.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "system-diff",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "A pane beside the chat: what Claude is working on in your system, zoomed in, what just happened and why it matters, and a topic to learn (system-diff)",
|
|
5
5
|
"types": "./types/index.d.ts",
|
|
6
6
|
"author": {
|
|
@@ -12,7 +12,7 @@ import type { Page, PageBlock, Row, Run } from '../types'
|
|
|
12
12
|
// the rest keeps its colours: yellow what changed, green what is new, red what went, gray the background.
|
|
13
13
|
|
|
14
14
|
type Props = { page: Page; canGoBack: boolean }
|
|
15
|
-
type State = { sel: string; sx: number; mine: boolean; ticking?: boolean }
|
|
15
|
+
type State = { sel: string; sx: number; mine: boolean; ticking?: boolean; free?: boolean } // free: scrolled by hand
|
|
16
16
|
|
|
17
17
|
const COLORS: Record<string, string> = {
|
|
18
18
|
accent: 'blue', focus: 'blue', near: 'blue', data: 'blue',
|
|
@@ -88,7 +88,7 @@ const PageView: ClientModule<JsonValue, State> = (raw, surface) => {
|
|
|
88
88
|
const state = surface.state
|
|
89
89
|
const sel = state?.mine && byId.has(state.sel) ? state.sel : byId.has(page.pick) ? page.pick : (page.blocks[0]?.id ?? '')
|
|
90
90
|
const picked = byId.get(sel)
|
|
91
|
-
const sx = across(page, cols, state?.mine ? state.sx : 0, picked)
|
|
91
|
+
const sx = across(page, cols, state?.mine ? state.sx : 0, state?.free ? undefined : picked)
|
|
92
92
|
|
|
93
93
|
// The pane was laid out for another width (the split moved): ask for this one once it holds, not for every
|
|
94
94
|
// width a drag passes on its way.
|
|
@@ -104,7 +104,8 @@ const PageView: ClientModule<JsonValue, State> = (raw, surface) => {
|
|
|
104
104
|
surface.post({ width: askedWidth })
|
|
105
105
|
}
|
|
106
106
|
})
|
|
107
|
-
surface.setState({ sel: state?.sel ?? '', sx: state?.sx ?? 0, mine: state?.mine ?? false, ticking: true
|
|
107
|
+
surface.setState({ sel: state?.sel ?? '', sx: state?.sx ?? 0, mine: state?.mine ?? false, ticking: true,
|
|
108
|
+
free: state?.free })
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
const pick = (b: PageBlock | undefined, x = sx) => {
|
|
@@ -127,8 +128,10 @@ const PageView: ClientModule<JsonValue, State> = (raw, surface) => {
|
|
|
127
128
|
} else if (['up', 'down', 'left', 'right'].includes(e.key)) {
|
|
128
129
|
pick(move(page.blocks, picked, e.key))
|
|
129
130
|
} else if (e.key === '[' || e.key === ']') {
|
|
130
|
-
surface.setState({ sel, sx: across(page, cols, sx + (e.key === '[' ? -STEP : STEP), undefined), mine: true,
|
|
131
|
+
surface.setState({ sel, sx: across(page, cols, sx + (e.key === '[' ? -STEP : STEP), undefined), mine: true, free: true,
|
|
131
132
|
ticking: true })
|
|
133
|
+
} else if (e.key === '-' || e.key === '=' || e.key === '+') {
|
|
134
|
+
surface.post({ zoomBy: e.key === '-' ? 1 : -1 }) // - shows more of the system, = bigger boxes
|
|
132
135
|
} else if (e.key === 'return') {
|
|
133
136
|
act(picked, 'enter')
|
|
134
137
|
} else if (e.key === ' ') {
|
|
@@ -185,3 +185,15 @@ test('/system-diff close closes the pane, and so does its close button', async (
|
|
|
185
185
|
expect(closed).toEqual(['system-diff', 'system-diff'])
|
|
186
186
|
await ui.unmount()
|
|
187
187
|
})
|
|
188
|
+
|
|
189
|
+
test('zoom out draws the diagrams compact, zoom in full again', async ($, on) => {
|
|
190
|
+
const asked: string[][] = []
|
|
191
|
+
answer(on, asked)
|
|
192
|
+
await $.command.run(RUN)
|
|
193
|
+
const ui = await $.ui.mount({ ...PANE, surface: 'terminal' })
|
|
194
|
+
await ui.press({ key: 'zoom-out' })
|
|
195
|
+
expect(asked[asked.length - 1]?.join(' ')).toContain('--zoom 1')
|
|
196
|
+
await ui.key({ key: '=', in: 'page' })
|
|
197
|
+
expect(asked[asked.length - 1]?.join(' ')).toContain('--zoom 0')
|
|
198
|
+
await ui.unmount()
|
|
199
|
+
})
|
|
@@ -14,7 +14,7 @@ import type { Act, Page, Row } from '../types'
|
|
|
14
14
|
// green what is new, done or to learn, white what to read, gray the background, red only what was taken away.
|
|
15
15
|
|
|
16
16
|
const PANE = 'system-diff'
|
|
17
|
-
const VERSION = '0.4.
|
|
17
|
+
const VERSION = '0.4.2' // the plugin's own, as .claude-plugin/plugin.json says (a test keeps them one)
|
|
18
18
|
const WIDEST = 999 // as wide as Claude Code docks a pane: 45% of the terminal
|
|
19
19
|
const COMMANDS = ['system-diff', 'system-diff-pane'] as const
|
|
20
20
|
const VIEWS = [['now', 'Now', 'n'], ['map', 'Map', 'm'], ['agents', 'Agents', 'a'], ['changes', 'Changes', 'c'],
|
|
@@ -25,6 +25,9 @@ const view = atom({ plugin: 'system-diff', key: 'view' } as const, 'now', { shap
|
|
|
25
25
|
const part = atom({ plugin: 'system-diff', key: 'part' } as const, '', { shape: 'part-1' })
|
|
26
26
|
const back = atom({ plugin: 'system-diff', key: 'back' } as const, [] as string[], { shape: 'back-2' })
|
|
27
27
|
const note = atom({ plugin: 'system-diff', key: 'note' } as const, 'Reading the system…', { shape: 'note-1' })
|
|
28
|
+
// How compact the diagrams are: -1 by the pane's width (compact below 110 columns), 0 full, 1 compact, 2 tiny.
|
|
29
|
+
const zoomLevel = atom({ plugin: 'system-diff', key: 'zoom' } as const, -1, { shape: 'zoom-1' })
|
|
30
|
+
const ZOOM_WORD = ['full', 'compact', 'tiny']
|
|
28
31
|
|
|
29
32
|
// How to run system-diff: the repository's own virtualenv first, then whatever is on PATH.
|
|
30
33
|
const RUNNERS: string[][] = [
|
|
@@ -73,6 +76,8 @@ async function refresh($: EngineInterface): Promise<void> {
|
|
|
73
76
|
again = false
|
|
74
77
|
const argv = ['panel', '--protocol', '2', '--width', String(columns), '--view', await read($, view),
|
|
75
78
|
'--part', await read($, part)]
|
|
79
|
+
const level = await read($, zoomLevel)
|
|
80
|
+
if (level >= 0) argv.push('--zoom', String(level)) // left out, the engine picks by the width (older ones too)
|
|
76
81
|
const got = await run($, argv)
|
|
77
82
|
if (got && !got.old) {
|
|
78
83
|
if (got.out !== last) {
|
|
@@ -155,6 +160,13 @@ async function scrollBy($: EngineInterface, rows: number): Promise<void> {
|
|
|
155
160
|
|
|
156
161
|
// As wide as Claude Code docks a pane, the first time: a width the person set (dragged, or with the pane's keys:
|
|
157
162
|
// ctrl+x ← →) is theirs from then on.
|
|
163
|
+
// More of the system in the pane (zoom out, +1) or bigger boxes (zoom in, -1), from where the page is drawn now.
|
|
164
|
+
async function zoomBy($: EngineInterface, by: number): Promise<void> {
|
|
165
|
+
const drawnAt = (await read($, page))?.zoom ?? 0
|
|
166
|
+
await update($, zoomLevel, () => Math.max(0, Math.min(2, drawnAt + by)))
|
|
167
|
+
await refresh($)
|
|
168
|
+
}
|
|
169
|
+
|
|
158
170
|
function open($: EngineInterface) {
|
|
159
171
|
return $.ui.open({ id: PANE, title: 'System diff', columns: WIDEST })
|
|
160
172
|
}
|
|
@@ -213,7 +225,7 @@ export const register: Register = on => {
|
|
|
213
225
|
// What the page (page.tsx) asks for.
|
|
214
226
|
on('ui.message', { component: 'Pane', requestId: PANE }, async ($, e) => {
|
|
215
227
|
const data = (e.data ?? {}) as Act & { back?: boolean; follow?: boolean; width?: number; reveal?: number;
|
|
216
|
-
scrollBy?: number }
|
|
228
|
+
scrollBy?: number; zoomBy?: number }
|
|
217
229
|
if (typeof data.width === 'number' && data.width > 0 && data.width !== columns) {
|
|
218
230
|
columns = data.width
|
|
219
231
|
await refresh($)
|
|
@@ -223,6 +235,7 @@ export const register: Register = on => {
|
|
|
223
235
|
else if (typeof data.learn === 'string') await learn($, data.learn)
|
|
224
236
|
else if (data.back) await goBack($)
|
|
225
237
|
else if (data.follow) await follow($)
|
|
238
|
+
else if (typeof data.zoomBy === 'number') await zoomBy($, data.zoomBy)
|
|
226
239
|
else if (typeof data.view === 'string') await choose($, data.view)
|
|
227
240
|
return {}
|
|
228
241
|
})
|
|
@@ -278,11 +291,14 @@ export const register: Register = on => {
|
|
|
278
291
|
{behind.length > 0 && (
|
|
279
292
|
<Button key="back" hotkey="b" plain onPress={() => void goBack($)}><Text dimColor>← Back</Text></Button>
|
|
280
293
|
)}
|
|
294
|
+
<Button key="zoom-out" plain onPress={() => void zoomBy($, 1)}><Text dimColor>⊖</Text></Button>
|
|
295
|
+
<Text dimColor>{ZOOM_WORD[drawn?.zoom ?? 0] ?? 'full'}</Text>
|
|
296
|
+
<Button key="zoom-in" plain onPress={() => void zoomBy($, -1)}><Text dimColor>⊕</Text></Button>
|
|
281
297
|
<Button key="close" plain onPress={() => void $.ui.close({ id: PANE })}><Text dimColor>× close</Text></Button>
|
|
282
298
|
</Box>
|
|
283
299
|
{said !== '' && <Text dimColor>{said}</Text>}
|
|
284
300
|
{drawn !== null && Client !== undefined && (
|
|
285
|
-
<Client key="page" module="./page.tsx" props={{ page: drawn, canGoBack: behind.length > 0 }} />
|
|
301
|
+
<Client key="page" module="./page.tsx" width="100%" props={{ page: drawn, canGoBack: behind.length > 0 }} />
|
|
286
302
|
)}
|
|
287
303
|
{drawn !== null && Client === undefined && drawn.rows.map(line)}
|
|
288
304
|
{drawn !== null && Client === undefined && (
|
|
@@ -24,6 +24,7 @@ export type Page = {
|
|
|
24
24
|
lights: Record<string, string[]>
|
|
25
25
|
details: Record<string, Row[]>
|
|
26
26
|
pick: string
|
|
27
|
+
zoom?: number // how compact it is drawn: 0 full, 1 compact, 2 tiny
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
declare module 'claude-code' {
|
|
@@ -34,6 +35,7 @@ declare module 'claude-code' {
|
|
|
34
35
|
part: Shaped<string>
|
|
35
36
|
back: Shaped<string[]>
|
|
36
37
|
note: Shaped<string>
|
|
38
|
+
zoom: Shaped<number>
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
}
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "system-diff"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.2"
|
|
8
8
|
description = "Draws your system from its code (parts, entry points, data, outside systems) and what each change adds, changes or removes. No dependencies; SVG pictures for PRs and READMEs."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -24,6 +24,7 @@ from .sources import git_dir
|
|
|
24
24
|
|
|
25
25
|
STATE, BUS = "agents.json", "bus.jsonl"
|
|
26
26
|
MAIN = "main"
|
|
27
|
+
SYSTEM = "system-diff" # the sender of what system-diff itself publishes
|
|
27
28
|
KEEP = 2000 # messages kept on the bus
|
|
28
29
|
MOST_AGENTS = 4 # a plan's agents at most, unless asked for more
|
|
29
30
|
|
|
@@ -33,10 +34,11 @@ def _load(root: Path) -> Dict[str, Any]:
|
|
|
33
34
|
data = json.loads((git_dir(root) / STATE).read_text(encoding="utf-8"))
|
|
34
35
|
if isinstance(data, dict):
|
|
35
36
|
return {"plan": data.get("plan") or {}, "claims": data.get("claims") or {},
|
|
36
|
-
"agents": data.get("agents") or {}, "ids": data.get("ids") or {}
|
|
37
|
+
"agents": data.get("agents") or {}, "ids": data.get("ids") or {},
|
|
38
|
+
"warned": data.get("warned") or {}}
|
|
37
39
|
except (OSError, ValueError):
|
|
38
40
|
pass
|
|
39
|
-
return {"plan": {}, "claims": {}, "agents": {}, "ids": {}}
|
|
41
|
+
return {"plan": {}, "claims": {}, "agents": {}, "ids": {}, "warned": {}}
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
def _save(root: Path, data: Dict[str, Any]) -> None:
|
|
@@ -182,7 +184,60 @@ def after_edit(root: Path, agent_id: str, part: Optional[str], notes: List[Dict[
|
|
|
182
184
|
if part and claimed and claimed != name:
|
|
183
185
|
warn = f"{part} is claimed by agent {claimed}; {name} just edited it"
|
|
184
186
|
publish(root, f"agent:{claimed}", f"{name} edited your block {part}", sender=name, kind="warning")
|
|
185
|
-
|
|
187
|
+
from . import config
|
|
188
|
+
wide = spread(root, name, agent_id, bp, config.lenient(root))
|
|
189
|
+
# spread warnings reach the agent they are about directly; on the bus they are a record for the inbox tool
|
|
190
|
+
unread = [m for m in inbox(root, name) if not (m.get("from") == SYSTEM and m.get("kind") == "warning")]
|
|
191
|
+
context = "\n".join(t for t in (as_context(unread), wide) if t)
|
|
192
|
+
warn = "; ".join(t for t in (warn, wide) if t)
|
|
193
|
+
return {"context": context, "warn": warn, "name": name}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def areas_of(root: Path, bp: dict, cfg: Dict[str, Any]) -> Dict[str, str]:
|
|
197
|
+
"""Each block's area: the agent the saved plan gave it to, when there is a plan; else its container, or the
|
|
198
|
+
block itself when it is in none."""
|
|
199
|
+
agents = (_load(root)["plan"] or {}).get("agents") or []
|
|
200
|
+
if agents:
|
|
201
|
+
return {b: g["name"] for g in agents for b in g.get("blocks") or []}
|
|
202
|
+
from .architecture import groups_of
|
|
203
|
+
group = groups_of(bp, cfg, design.kinds(bp, cfg))
|
|
204
|
+
return {cid: (group[cid][2:] if group.get(cid, "").startswith("g:") else cid) for cid in bp["components"]}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def spread(root: Path, name: str, agent_id: str, bp: dict, cfg: Dict[str, Any]) -> str:
|
|
208
|
+
"""When one agent has edited blocks in two or more areas this turn: a message telling it to plan a team and hand
|
|
209
|
+
the rest on, once per agent per turn (also published to all); else ""."""
|
|
210
|
+
from .extract import Code
|
|
211
|
+
code = Code(cfg)
|
|
212
|
+
me = "" if agent_id in ("", None, MAIN) else agent_id
|
|
213
|
+
start, parts = 0.0, []
|
|
214
|
+
for e in activity.events(root, 400):
|
|
215
|
+
if e.get("event") == activity.PROMPT and not activity.is_system(e.get("text", "")):
|
|
216
|
+
start, parts = e["at"], []
|
|
217
|
+
elif e.get("event") == activity.EDIT and (e.get("agent") or "") == me and e.get("path"):
|
|
218
|
+
part = activity.part_of(e["path"], code)
|
|
219
|
+
if part and part not in parts:
|
|
220
|
+
parts.append(part)
|
|
221
|
+
area = areas_of(root, bp, cfg)
|
|
222
|
+
seen: List[str] = []
|
|
223
|
+
for p in parts:
|
|
224
|
+
a = area.get(p, p)
|
|
225
|
+
if a not in seen:
|
|
226
|
+
seen.append(a)
|
|
227
|
+
if len(seen) < 2:
|
|
228
|
+
return ""
|
|
229
|
+
data = _load(root)
|
|
230
|
+
turn = str(start)
|
|
231
|
+
told = data["warned"].get(turn) or []
|
|
232
|
+
if name in told:
|
|
233
|
+
return ""
|
|
234
|
+
data["warned"] = {turn: told + [name]} # only the latest turn is kept
|
|
235
|
+
_save(root, data)
|
|
236
|
+
text = (f"{name} has edited blocks in {len(seen)} areas this turn ({', '.join(seen)}). system-diff runs work "
|
|
237
|
+
"that spans areas as a team: call plan_agents with what is left, launch its agents in parallel, and "
|
|
238
|
+
"keep to one area yourself.")
|
|
239
|
+
publish(root, "all", text, sender=SYSTEM, kind="warning")
|
|
240
|
+
return text
|
|
186
241
|
|
|
187
242
|
|
|
188
243
|
def who_worked(events: Iterable[dict], code) -> Dict[str, Dict[str, Any]]:
|
|
@@ -138,8 +138,12 @@ def title_style(n: dict) -> str:
|
|
|
138
138
|
return f"title bold {st}" if st != "same" else "title"
|
|
139
139
|
|
|
140
140
|
|
|
141
|
+
ZOOMS = {0: (20, 32, 2, 6, 4), 1: (12, 22, 0, 5, 3), 2: (8, 16, 0, 5, 3)} # min box, max box, rows, channel, gap
|
|
142
|
+
# by zoom: full, compact, tiny
|
|
143
|
+
|
|
144
|
+
|
|
141
145
|
def layered(nodes: List[dict], edges: List[dict], columns: List[Sequence[str]], *, width: int = 100,
|
|
142
|
-
centre: bool = False, most_lines: int = 2) -> Grid:
|
|
146
|
+
centre: bool = False, most_lines: int = 2, zoom: int = 0) -> Grid:
|
|
143
147
|
"""A system diagram: `nodes` (id, col, title, sub, status, type: actor|component|group|store|host; `agent` where
|
|
144
148
|
the agent works, `focus` for the box zoomed in on, `items` for the components a container holds, `lines` for
|
|
145
149
|
rows inside a box) in `columns` order, joined by `edges` (from, to, kind, status, label; `from_item`/`to_item`:
|
|
@@ -150,6 +154,12 @@ def layered(nodes: List[dict], edges: List[dict], columns: List[Sequence[str]],
|
|
|
150
154
|
present = [k for k, _h in columns if any(n["col"] == k for n in nodes)]
|
|
151
155
|
if not present:
|
|
152
156
|
return g
|
|
157
|
+
least, most_w, rows_in, chan_min, chan_pad = ZOOMS.get(zoom, ZOOMS[0])
|
|
158
|
+
most_lines = min(most_lines, rows_in)
|
|
159
|
+
if zoom: # compact: no labels on flows (a picked box's detail names them), tiny: no subtitles either
|
|
160
|
+
edges = [{**e, "label": ""} for e in edges]
|
|
161
|
+
if zoom >= 2:
|
|
162
|
+
nodes = [{**n, "sub": ""} for n in nodes]
|
|
153
163
|
heading = {k: h for k, h in columns}
|
|
154
164
|
ci = {k: i for i, k in enumerate(present)}
|
|
155
165
|
col_nodes = [[n for n in nodes if n["col"] == k] for k in present]
|
|
@@ -181,13 +191,13 @@ def layered(nodes: List[dict], edges: List[dict], columns: List[Sequence[str]],
|
|
|
181
191
|
item_row = {it["id"]: (n["id"], 3 + j) for n in nodes for j, it in enumerate(n.get("items") or [])}
|
|
182
192
|
|
|
183
193
|
# widths: as wide as the longest row wants, narrowed together when the whole is wider than the pane
|
|
184
|
-
want = [min(
|
|
185
|
-
|
|
186
|
-
|
|
194
|
+
want = [min(most_w, max([len(t) + 4 for n in col for t, _s, _b in contents[n["id"]][:1]]
|
|
195
|
+
+ [len(t) + 4 for n in col for t, _s, _b in contents[n["id"]][2:]]
|
|
196
|
+
+ [min(len(heading[present[i]]) + 1, most_w), least]))
|
|
187
197
|
for i, col in enumerate(col_nodes)]
|
|
188
198
|
label_w = max([len(e.get("label") or "") for e in edges] + [0])
|
|
189
|
-
gaps = (
|
|
190
|
-
while sum(want) + gaps > width and max(want) >
|
|
199
|
+
gaps = (chan_min + 2 + label_w) * len(present)
|
|
200
|
+
while sum(want) + gaps > width and max(want) > least:
|
|
191
201
|
biggest = max(range(len(want)), key=lambda i: want[i])
|
|
192
202
|
want[biggest] -= 1
|
|
193
203
|
heights = {n["id"]: 2 + len(contents[n["id"]]) for n in nodes}
|
|
@@ -320,7 +330,7 @@ def layered(nodes: List[dict], edges: List[dict], columns: List[Sequence[str]],
|
|
|
320
330
|
if e.get("label") and cs != ct:
|
|
321
331
|
label_in[hops[-1][0]] = max(label_in[hops[-1][0]], len(e["label"]) + 2)
|
|
322
332
|
widths = want
|
|
323
|
-
chan = [max(
|
|
333
|
+
chan = [max(chan_min, len(lanes[k]) + chan_pad + label_in[k]) for k in range(len(present))]
|
|
324
334
|
if not lanes[len(present) - 1]:
|
|
325
335
|
chan[-1] = 0
|
|
326
336
|
xs, x = [], 0
|
|
@@ -195,6 +195,7 @@ def parser() -> argparse.ArgumentParser:
|
|
|
195
195
|
pn.add_argument("--width", type=int, default=80)
|
|
196
196
|
pn.add_argument("--view", default="now", choices=["now", "map", "changes", "topics", "agents"])
|
|
197
197
|
pn.add_argument("--part", default="", help="the part to zoom in on (default: the one the agent works on)")
|
|
198
|
+
pn.add_argument("--zoom", type=int, default=-1, help="diagrams 0 full, 1 compact, 2 tiny; -1 by the width")
|
|
198
199
|
pn.add_argument("--protocol", type=int, default=1, help="2: a page of runs and pickable blocks (the mod's painter)")
|
|
199
200
|
pf = sub.add_parser("profile", help="what you want explained: technologies marked new to you")
|
|
200
201
|
pf.add_argument("--learn", metavar="TECH", help="mark a technology new to you, or unmark it")
|
|
@@ -254,7 +255,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
254
255
|
return agent.run_hook(sys.stdin.read() if not sys.stdin.isatty() else "")
|
|
255
256
|
if args.cmd == "panel":
|
|
256
257
|
from . import panel
|
|
257
|
-
return panel.main(root, args.width, args.view, args.part, args.protocol)
|
|
258
|
+
return panel.main(root, args.width, args.view, args.part, args.protocol, args.zoom)
|
|
258
259
|
if args.cmd == "profile":
|
|
259
260
|
from . import profile
|
|
260
261
|
if args.learn:
|
|
@@ -35,7 +35,7 @@ def _name(nid: str, *bps: dict) -> str:
|
|
|
35
35
|
def _diagram(page: canvas.Page, c, model: Dict[str, Any], width: int, centre: bool = False) -> canvas.Grid:
|
|
36
36
|
"""The diagram on the page, every box a block: Enter zooms in on it; its detail says what it uses and what uses
|
|
37
37
|
it, all of it (the diagram draws each box's main flows)."""
|
|
38
|
-
g = canvas.layered(model["nodes"], model["edges"], model["columns"], width=width, centre=centre)
|
|
38
|
+
g = canvas.layered(model["nodes"], model["edges"], model["columns"], width=width, centre=centre, zoom=ZOOM)
|
|
39
39
|
conns = graph.connections(c.after)
|
|
40
40
|
names = {n["id"]: n["title"] for n in model["nodes"]}
|
|
41
41
|
names.update({i["id"]: i["title"] for n in model["nodes"] for i in n.get("items") or []})
|
|
@@ -476,7 +476,19 @@ def view_agents(root: Path, width: int) -> Dict[str, Any]:
|
|
|
476
476
|
return page.json(view="agents", part="")
|
|
477
477
|
|
|
478
478
|
|
|
479
|
-
|
|
479
|
+
ZOOM = 0 # how compact the diagrams are drawn: 0 full, 1 compact, 2 tiny (show sets it)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def show(root: Path, width: int = 80, view: str = "now", part: Optional[str] = None, zoom: int = -1) -> Dict[str, Any]:
|
|
483
|
+
"""The page for a view. `zoom` -1: compact below 110 columns, full above."""
|
|
484
|
+
global ZOOM
|
|
485
|
+
ZOOM = (1 if width < 110 else 0) if zoom < 0 else max(0, min(2, zoom))
|
|
486
|
+
out = _show(root, width, view, part)
|
|
487
|
+
out["zoom"] = ZOOM
|
|
488
|
+
return out
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _show(root: Path, width: int = 80, view: str = "now", part: Optional[str] = None) -> Dict[str, Any]:
|
|
480
492
|
if view == "agents":
|
|
481
493
|
return view_agents(root, width)
|
|
482
494
|
if view == "map":
|
|
@@ -232,10 +232,11 @@ def rows(root: Path, width: int = 80) -> List[textmap.Line]:
|
|
|
232
232
|
return got["head"] + got["rows"]
|
|
233
233
|
|
|
234
234
|
|
|
235
|
-
def main(root: Path, width: int, view: str = "now", part: Optional[str] = None, protocol: int = 1
|
|
235
|
+
def main(root: Path, width: int, view: str = "now", part: Optional[str] = None, protocol: int = 1,
|
|
236
|
+
zoom: int = -1) -> int:
|
|
236
237
|
if protocol >= 2:
|
|
237
238
|
from . import pane
|
|
238
|
-
print(json.dumps(pane.show(root, width, view, part or None), ensure_ascii=False))
|
|
239
|
+
print(json.dumps(pane.show(root, width, view, part or None, zoom), ensure_ascii=False))
|
|
239
240
|
return 0
|
|
240
241
|
got = show(root, width, view, part or None)
|
|
241
242
|
for key in ("head", "rows"):
|
|
@@ -18,7 +18,7 @@ REFUND = ('from fastapi import APIRouter\nrouter = APIRouter()\n\n\n@router.post
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def page(capsys, repo, *args):
|
|
21
|
-
assert main(["--repo", str(repo), "panel", "--width", "100", "--protocol", "2", *args]) == 0
|
|
21
|
+
assert main(["--repo", str(repo), "panel", "--width", "100", "--protocol", "2", "--zoom", "0", *args]) == 0
|
|
22
22
|
out = json.loads(capsys.readouterr().out)
|
|
23
23
|
return out, ["".join(run[0] for run in row) for row in out["rows"]]
|
|
24
24
|
|
|
@@ -268,3 +268,51 @@ def test_the_agents_tab_shows_each_agent_and_warns_when_two_edit_one_block(repo,
|
|
|
268
268
|
assert any(t.startswith("THE TEAM plan: make the API") for t in text)
|
|
269
269
|
assert any("api and store all edit API" in t for t in text)
|
|
270
270
|
assert "● API @api @st" in "\n".join(text) # who works where, on the diagram
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_one_agent_editing_two_areas_in_a_turn_is_told_once_to_plan_a_team(repo, files):
|
|
274
|
+
from system_diff import agent
|
|
275
|
+
|
|
276
|
+
def prompt():
|
|
277
|
+
agent.run_hook(json.dumps({"cwd": str(repo), "hook_event_name": "UserPromptSubmit", "prompt": "go"}),
|
|
278
|
+
emit=lambda _s: None)
|
|
279
|
+
|
|
280
|
+
def edit(path, text, **who):
|
|
281
|
+
files(repo, {path: text})
|
|
282
|
+
said: list = []
|
|
283
|
+
agent.run_hook(json.dumps({"cwd": str(repo), "hook_event_name": "PostToolUse", "tool_name": "Edit", **who,
|
|
284
|
+
"tool_input": {"file_path": str(repo / path)}}), emit=said.append)
|
|
285
|
+
outs = [json.loads(s) for s in said]
|
|
286
|
+
warns = " ".join(o.get("systemMessage", "") for o in outs)
|
|
287
|
+
context = " ".join(o.get("hookSpecificOutput", {}).get("additionalContext", "") for o in outs)
|
|
288
|
+
return warns, context
|
|
289
|
+
|
|
290
|
+
routes, rules = "app/api/routes.py", "app/core/rules.py"
|
|
291
|
+
prompt()
|
|
292
|
+
warns, context = edit(routes, (repo / routes).read_text() + "\n# one\n")
|
|
293
|
+
assert "areas this turn" not in warns + context # one area so far
|
|
294
|
+
warns, context = edit(rules, (repo / rules).read_text() + "\n# one\n")
|
|
295
|
+
assert "main has edited blocks in 2 areas this turn" in warns and "plan_agents" in warns
|
|
296
|
+
assert "main has edited blocks in 2 areas this turn" in context and "plan_agents" in context
|
|
297
|
+
warns, context = edit("app/db/repo.py", (repo / "app/db/repo.py").read_text() + "\n# one\n")
|
|
298
|
+
assert "areas this turn" not in warns + context # once a turn
|
|
299
|
+
prompt()
|
|
300
|
+
edit(routes, (repo / routes).read_text() + "# two\n")
|
|
301
|
+
warns, context = edit(rules, (repo / rules).read_text() + "# two\n")
|
|
302
|
+
assert "areas this turn" in warns and "areas this turn" in context # a new turn may warn again
|
|
303
|
+
prompt()
|
|
304
|
+
for text in ("# a\n", "# b\n"): # a subagent staying in its one area
|
|
305
|
+
warns, context = edit(routes, (repo / routes).read_text() + text, agent_id="a1")
|
|
306
|
+
assert "areas this turn" not in warns and "a1 has edited" not in context # main's warning may reach it by bus
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def test_a_narrow_pane_gets_compact_diagrams_unless_asked_otherwise(repo, capsys):
|
|
310
|
+
narrow = json.loads(_run(capsys, repo, "--width", "90"))
|
|
311
|
+
wide = json.loads(_run(capsys, repo, "--width", "140"))
|
|
312
|
+
assert narrow["zoom"] == 1 and wide["zoom"] == 0
|
|
313
|
+
assert json.loads(_run(capsys, repo, "--width", "90", "--zoom", "2"))["zoom"] == 2
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _run(capsys, repo, *args):
|
|
317
|
+
assert main(["--repo", str(repo), "panel", "--protocol", "2", *args]) == 0
|
|
318
|
+
return capsys.readouterr().out
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|