Skip to content

Commit 9f3c731

Browse files
committed
improve Fix tab UX: persistent graph, smarter navigation, incremental fixes
- Dependency graph now stays visible while re-analyzing - Graph shows container boxes for composed layers with nested children - Tab key cycles focus between graph, services list, and candidates - Arrow keys (h/l) for quick panel switching, j/k for list navigation - Apply fix [p] works from any panel, not just when graph is hidden - New layers merge into existing AppLayer instead of replacing it - Auto-analyze when switching to Fix tab for first time - Removed 'applied' status - analysis stays complete after applying
1 parent ffc51d3 commit 9f3c731

13 files changed

Lines changed: 705 additions & 162 deletions

src/AnalysisStatusView.tsx

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Analysis Status View Component
33
*
4-
* Shows different analysis states: idle, analyzing, complete (no results), error, applied.
4+
* Shows different analysis states: idle, analyzing, complete (no results), error.
55
*/
66

77
import { Show } from "solid-js";
@@ -79,21 +79,6 @@ export function AnalysisStatusView() {
7979
</text>
8080
<text style={{ fg: COLORS.muted }}>Press [?] for help</text>
8181
</Show>
82-
83-
<Show when={store.ui.layerAnalysisStatus === "applied"}>
84-
<text style={{ fg: COLORS.success }} marginBottom={2}>
85-
Fix applied successfully!
86-
</text>
87-
<Show when={store.ui.layerAnalysisResults}>
88-
<text style={{ fg: COLORS.text }} marginBottom={1}>
89-
{`Modified: ${store.ui.layerAnalysisResults?.targetFile}:${store.ui.layerAnalysisResults?.targetLine}`}
90-
</text>
91-
<text style={{ fg: COLORS.text }} marginBottom={2}>
92-
Added: Layer composition
93-
</text>
94-
</Show>
95-
<text style={{ fg: COLORS.muted }}>Press [a] to re-analyze</text>
96-
</Show>
9782
</box>
9883
);
9984
}

src/DependencyGraphView.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ interface DependencyGraphViewProps {
2929
layers: LayerDefinition[];
3030
selectedNode?: string;
3131
onSelectNode?: (name: string) => void;
32+
focused?: boolean;
3233
}
3334

3435
/**
@@ -38,12 +39,14 @@ function GraphHeader(props: {
3839
layerCount: number;
3940
cycleCount: number;
4041
orphanCount: number;
42+
focused?: boolean;
4143
}) {
4244
return (
4345
<box flexDirection="column" marginBottom={1}>
4446
<box flexDirection="row" gap={2}>
45-
<text style={{ fg: COLORS.primary }}>
46-
Dependency Graph ({props.layerCount} layers)
47+
<text style={{ fg: props.focused ? COLORS.primary : COLORS.muted }}>
48+
{props.focused ? "> " : " "}Dependency Graph ({props.layerCount}{" "}
49+
layers)
4750
</text>
4851
</box>
4952

@@ -141,11 +144,12 @@ export function DependencyGraphView(props: DependencyGraphViewProps) {
141144
layerCount={props.layers.length}
142145
cycleCount={cycles().length}
143146
orphanCount={orphans().length}
147+
focused={props.focused}
144148
/>
145149

146150
<GraphLegend />
147151

148-
<scrollbox flexGrow={1} focused>
152+
<scrollbox flexGrow={1} focused={props.focused ?? false}>
149153
<For each={graphLines()}>
150154
{(line) => (
151155
<GraphLine
@@ -166,6 +170,8 @@ export function DependencyGraphView(props: DependencyGraphViewProps) {
166170
export function DependencyGraphPanel() {
167171
const { store } = useStore();
168172

173+
const isFocused = createMemo(() => store.ui.fixTabFocusedPanel === "graph");
174+
169175
const layers = createMemo((): LayerDefinition[] => {
170176
// Get layers from analysis results if available
171177
const results = store.ui.layerAnalysisResults;
@@ -186,6 +192,8 @@ export function DependencyGraphPanel() {
186192
allLayers.push({
187193
...layer,
188194
provides: candidate.service, // The service this candidate provides
195+
composedOf: layer.composedOf ?? [],
196+
compositionType: layer.compositionType ?? "none",
189197
});
190198
}
191199
}
@@ -216,7 +224,7 @@ export function DependencyGraphPanel() {
216224
</box>
217225
}
218226
>
219-
<DependencyGraphView layers={layers()} />
227+
<DependencyGraphView layers={layers()} focused={isFocused()} />
220228
</Show>
221229
);
222230
}

src/codemod.ts

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -178,25 +178,60 @@ export function applyLayerFix(fix: LayerFix): CodemodResult {
178178

179179
// Determine what we need to do
180180
if (existingAppLayerDecl) {
181-
// Case 2: Update existing AppLayer
182-
log("[Codemod] Case 2: Updating existing AppLayer");
183-
184-
// Use the generated code directly - it already includes proper layer composition
185-
// with Layer.provideMerge for dependent layers
186-
log("[Codemod] Using generated layer code:", fix.generatedCode);
181+
// Case 2: Merge new layers into existing AppLayer
182+
log("[Codemod] Case 2: Merging into existing AppLayer");
183+
log("[Codemod] Existing layers:", existingLayerNames);
184+
log("[Codemod] New layers:", fix.layerNames);
187185

188186
// Parse the generated layer code to get its AST
189187
const layerExprAST = recast.parse(`(${fix.generatedCode})`, {
190188
parser: typescriptParser,
191189
});
192-
const layerExpr = layerExprAST.program.body[0].expression;
190+
const newLayerExpr = layerExprAST.program.body[0].expression;
193191

194-
// Update the existing declaration with the new expression
195192
const decl = existingAppLayerDecl.node.declarations[0];
196-
decl.init = layerExpr;
193+
194+
// If existing is already Layer.mergeAll, add new layers to its arguments
195+
// Otherwise, wrap both in a new Layer.mergeAll
196+
if (
197+
n.CallExpression.check(decl.init) &&
198+
n.MemberExpression.check(decl.init.callee) &&
199+
n.Identifier.check(decl.init.callee.object) &&
200+
decl.init.callee.object.name === "Layer" &&
201+
n.Identifier.check(decl.init.callee.property) &&
202+
decl.init.callee.property.name === "mergeAll"
203+
) {
204+
// Extract arguments from new expression if it's also Layer.mergeAll, otherwise add directly
205+
if (
206+
n.CallExpression.check(newLayerExpr) &&
207+
n.MemberExpression.check(newLayerExpr.callee) &&
208+
n.Identifier.check(newLayerExpr.callee.object) &&
209+
newLayerExpr.callee.object.name === "Layer" &&
210+
n.Identifier.check(newLayerExpr.callee.property) &&
211+
newLayerExpr.callee.property.name === "mergeAll"
212+
) {
213+
// Flatten: add each argument from the new Layer.mergeAll
214+
for (const arg of newLayerExpr.arguments) {
215+
decl.init.arguments.push(arg);
216+
}
217+
log("[Codemod] Flattened and appended Layer.mergeAll arguments");
218+
} else {
219+
// Add the expression directly
220+
decl.init.arguments.push(newLayerExpr);
221+
log("[Codemod] Appended to existing Layer.mergeAll");
222+
}
223+
} else {
224+
// Wrap existing and new in Layer.mergeAll
225+
const mergedExpr = b.callExpression(
226+
b.memberExpression(b.identifier("Layer"), b.identifier("mergeAll")),
227+
[decl.init, newLayerExpr],
228+
);
229+
decl.init = mergedExpr;
230+
log("[Codemod] Wrapped in new Layer.mergeAll");
231+
}
197232

198233
result.changes.push(
199-
`Updated AppLayer with layers: ${fix.layerNames.join(", ")}`,
234+
`Added layers to AppLayer: ${fix.layerNames.join(", ")}`,
200235
);
201236
} else {
202237
// Case 1: Create new AppLayer and wrap Effect.run
@@ -214,14 +249,6 @@ export function applyLayerFix(fix: LayerFix): CodemodResult {
214249
});
215250
const layerDeclaration = layerAST.program.body[0];
216251

217-
// Add a comment above the layer declaration
218-
const comment = b.commentBlock(
219-
" Auto-generated layer composition ",
220-
true,
221-
false,
222-
);
223-
layerDeclaration.comments = [comment];
224-
225252
// Insert the layer declaration before the statement
226253
insertionStatement.insertBefore(layerDeclaration);
227254
result.changes.push("Inserted AppLayer variable with layer composition");

0 commit comments

Comments
 (0)