| name | a9s-add-child-view |
| description | Blueprint for adding a new child view to a9s โ 3-phase workflow (scope -> QA tests -> coder implement) with exact file manifests and hard-won lessons |
| disable-model-invocation | true |
Adding a New Child View
Workflow: main session scopes -> QA + Coder execute (parallel-safe for child views).
Phase Ownership
| Phase | Owner | Writes to |
|---|
| Phase 1: Spec & scoping | Main session | Scoped task manifests only |
| Phase 2: Tests | a9s-qa | tests/unit/ only |
| Phase 3: Implementation | a9s-coder | internal/, cmd/, .a9s/ only |
Coder MUST NOT write test files. QA MUST NOT write production code.
Both subagents MUST reject tasks without an exact file scope.
Phase 1: Spec & scoping (main session)
The main session reads the design spec and parent fetcher, then produces two scoped tasks โ one for QA, one for coder. This prevents context drain โ the subagents receive only the manifest, not the full design spec.
Scoping must determine:
-
Parent analysis โ read core/aws/{parent}.go:
- What is
Resource.ID? (often a name, NOT an ARN)
- What Fields keys exist? Does the parent store the ARN? If not, add it.
- Which
ServiceClients field is needed? Already exists?
-
ContextKeys mapping โ the #1 source of bugs:
- If the child API needs an ARN, the parent MUST have the ARN in Fields
"ID" -> Resource.ID (often a name โ verify!)
"Name" -> Resource.Name
"field_key" -> Resource.Fields["field_key"]
"@parent.x" -> inherited from parent context (Pattern C nesting)
- Write a test that verifies the parent fetcher populates the required field
-
Field formatting rules (apply to ALL fields):
*int64 epoch-ms timestamps -> use formatEpochMillis() in fetcher, key: in config
*int64 byte counts -> use formatBytes() in fetcher, key: in config
- String SDK fields -> can use
path: in config (reads from RawStruct directly)
- Computed/formatted fields -> MUST use
key: in config (reads from Fields map)
- Never use
path: for timestamps or byte counts โ they show raw numbers
-
File manifest โ exact list of files to CREATE/EDIT/APPEND with specific content
Scoping output format (TWO tasks):
CHILD VIEW SPEC: {child_shortname}
Parent: {parent_shortname} | Pattern: {A/B/C/D} | Key: {enter/e/L/r/s}
API: {service}:{APICall} with {ParentParam}
Client field: c.{ServiceField} (exists: yes/no)
Parallelization: parallel-safe
CONTEXT KEYS:
{context_key} <- Fields["{field_key}"] (verified: parent stores ARN at line N)
COLUMNS (all use key: in config):
{key} | {Title} | {width} | formatter: {none/formatEpochMillis/formatBytes}
DETAIL PATHS:
{Path1}, {Path2}, ... (longest: {N} chars -> keyW will auto-size)
### CODER TASK:
Files to create:
core/aws/{child_type}.go โ child fetcher + init() with RegisterChildType/RegisterPaginatedChild/RegisterFieldKeys
Files to modify:
core/aws/{service}_interfaces.go โ append {InterfaceName} narrow interface AND embed it on the aggregate {Service}API in the same file
Append point: after last narrow interface, before the aggregate {Service}API
core/resource/types.go โ add Children to {parent}, add {ChildType}Columns()
Append point: grep "{parent_shortname}" in resourceTypes
core/config/defaults.go โ add "{child_shortname}" entry
Append point: last entry in defaultViews.Views map
.a9s/views/{child_shortname}.yaml โ regenerate via viewsgen
cmd/refgen/main.go โ append entry (if SDK struct)
Append point: last entry in resources slice
core/demo/fixtures/<service>.go โ add child fixture data to the parent service's fixture file
Append point: last fixture builder function in the service file
core/demo/fakes/<service>.go โ extend the fake to serve the child data
Append point: last method on the fake struct
Context files (read-only):
core/aws/{parent}.go โ parent fetcher for ContextKeys verification
core/aws/ec2.go โ canonical example
### QA TASK:
Test files to create:
tests/unit/aws_{child_shortname}_test.go โ fetcher tests
Test files to modify:
tests/unit/mocks_test.go โ append mock struct
Append point: last mock in file
tests/unit/qa_detail_child_views_test.go โ append 2 tests
Append point: last TestQA_Detail_ function
tests/unit/qa_yaml_child_views_test.go โ append 3 tests
Append point: last TestQA_YAML_ function
tests/unit/qa_list_rawstruct_child_views_test.go โ append 1 test
Append point: last TestQA_ListRawStruct_ function
Mock structure:
{exact mock struct + method signature}
Type signatures:
{interface + SDK types needed for compilable tests}
What to test:
- Happy path: {expected behavior}
- Empty response: {expected behavior}
- API error: {expected behavior}
- Pagination: {if applicable}
- Nil fields: no panic
- Parent context: verify correct key used
Context files (read-only):
core/aws/{service}_interfaces.go โ interface definition (after coder adds it)
core/resource/types.go โ column keys
Phase 2: Tests (a9s-qa agent)
The QA agent receives the scoped QA task and writes ALL tests.
Test files to create/modify:
1. Mock: tests/unit/mocks_test.go (APPEND)
- For paginated APIs, use
outputs []*{Output} slice with callIdx counter
- For single-response APIs, use
output *{Output}
2. Fetcher tests: tests/unit/aws_{child_shortname}_test.go (CREATE)
- Happy path: correct ID, Name, Status, all Fields, RawStruct
- Empty response: empty slice, no error
- API error: error propagation
- Pagination (if applicable): multiple pages collected, stops at cap
- Timestamp formatting: known epoch ms -> expected formatted string
- Byte formatting: known bytes -> expected human-readable string
- Nil fields: no panic, empty strings
- RawStruct: original SDK struct preserved
- Parent context test: verify the correct context key is used (e.g., ARN not name)
3. Detail tests: tests/unit/qa_detail_child_views_test.go (APPEND)
- ViewContainsExpectedFields
- NilFields (no panic)
- Long field names not truncated (if any path > 22 chars)
- Formatted timestamps in detail (not raw epoch ms)
4. YAML + List tests: (APPEND to existing files)
- YAML view contains fields, frame title, no ANSI in raw content
- List rawstruct renders correctly
5. Run go test โ confirm tests compile (or fail with expected missing-function errors if running before coder).
Phase 3: Implementation (a9s-coder agent)
The coder receives the scoped coder task and makes all tests pass.
Checklist (order matters):
1. Interface: core/aws/<service>_interfaces.go (APPEND to the service's per-service file; also embed on the aggregate <Service>API in the same file)
type {InterfaceName} interface {
{APICall}(ctx context.Context, params *{service}.{APICall}Input, optFns ...func(*{service}.Options)) (*{service}.{APICall}Output, error)
}
2. Client field (IF new service): core/aws/client.go
3. Child fetcher: core/aws/{child_type}.go (CREATE)
init() registers: RegisterFieldKeys, RegisterPaginatedChild, RegisterChildType
- Fetcher function with proper formatting:
- Timestamps:
formatEpochMillis(*field) โ NEVER fmt.Sprintf("%d", *field)
- Bytes:
formatBytes(*field) โ NEVER fmt.Sprintf("%d", *field)
- Messages: strip newlines if content may contain
\n
- For paginated APIs: cap at reasonable limit (e.g.,
const maxResults = 500)
- Column function returns
[]resource.Column with proper widths
4. Parent wiring: core/resource/types.go (EDIT)
- Add/append
Children on parent type
- Add column function
- ContextKeys must map to actual data โ if API needs ARN, use Fields key that has ARN
5. Config: core/config/defaults.go (ADD)
- List columns: use
Key: for computed fields, Path: only for string SDK fields
- Detail paths: include all relevant fields
6. Views config: .a9s/views/{child_shortname}.yaml (REGENERATE)
- Run:
go run ./cmd/viewsgen/
- This auto-generates from defaults.go โ do NOT edit view YAML files manually
7. Refgen: cmd/refgen/main.go (APPEND if SDK struct)
8. Demo fixtures:
Hybrid fixture pattern (014-demo-transport-mock). Demo mode has two layers: the legacy HTTP transport (core/demo/transport.go + handlers.go) is the base for all services, and per-service typed fakes (core/demo/fakes/<service>.go) override individual services. Currently only EC2 uses a typed fake.
- Preferred (migrated services): add fixture data to
core/demo/fixtures/<service>.go and extend the matching fake in core/demo/fakes/<service>.go.
- Legacy (non-migrated services): add fixture data to the matching
core/demo/fixtures_*.go category file and (if needed) extend handlers in core/demo/handlers.go.
When adding a new child view, match the parent service's current layer. Do not mix layers for the same service.
9. Parent fetcher (IF needed): add missing Fields (e.g., ARN)
Verification:
make test
make lint
make gofix
make build
go run ./cmd/viewsgen/ # always โ regenerate from defaults
go run ./cmd/refgen/ > .a9s/views_reference.yaml # if SDK struct added to refgen
Pattern Variants
Pattern A: Single Child (most common)
- Parent has 1 child.
Enter drills in.
- Examples: Target Group Health, ASG Activities, Alarm History, ECR Images
Pattern B: Multi-Child Parent
- 2+ children with different trigger keys.
- Examples: ECS (
Enter->Tasks, e->Events, L->Logs), CFN (Enter->Events, r->Resources)
- Implement all children of the same parent in ONE release.
Pattern C: Level-2 Nested
- Child has its own children.
RegisterChildType includes Children slice.
- Uses
@parent. prefix in ContextKeys.
- Examples: Log Streams->Events, Lambda Invocations->Log Lines, ELB Listeners->Rules
Pattern D: Cross-Service
- Fetcher calls different AWS service than parent.
- Needs multiple interfaces and possibly multiple client fields.
- Examples: Lambda->Invocations (CW Logs), ECS->Container Logs (CW Logs)
What You Do NOT Need to Change
app.go โ generic handleEnterChildView and fetchChildResources
messages.go โ EnterChildViewMsg handles all child navigation
resourcelist.go โ handleChildKey and buildChildContext
keys.go โ trigger keys already defined
Hard-Won Lessons (v3.1.0)
-
ContextKeys: ARN vs Name โ Resource.ID is often a name, not an ARN. If the child API needs an ARN, verify the parent populates it in Fields. Test this explicitly.
-
key: vs path: in config โ path: reads raw SDK struct (epoch ms, raw bytes). key: reads formatted Fields values. ALWAYS use key: for timestamps and byte counts.
-
Detail view Fields-first โ renderFromConfig checks Fields before RawStruct. Fetchers must populate Fields with ALL formatted values needed for detail display.
-
Newlines in messages โ PadOrTrunc strips \n/\r, but log-like messages should be cleaned in the fetcher too.
-
Narrow screens โ fitColumns shrinks the last column to remaining space (min 10 chars). Don't assume fixed terminal width.
-
Detail key column โ computeKeyWidth() auto-sizes from longest field name. Long dotted paths like Target.AvailabilityZone are handled.
-
Pagination caps โ large AWS resources (log groups with 8000+ streams) need pagination limits. Add const maxResults = 500 and break when exceeded.
-
Deprecated AWS fields โ StoredBytes on LogStream is deprecated (always 0). Check AWS docs before adding fields.
-
formatBytes/formatFloat are shared utilities in core/aws/log_streams.go โ reuse them, never delete.
-
Sort by age โ getAgeField matches field keys containing: time, date, launch, creation, event, start, timestamp. Name new time fields accordingly.