Skip to content

Commit 8804339

Browse files
committed
refactor: simplify modal state handling and section rendering
1 parent f10573e commit 8804339

24 files changed

+1045
-732
lines changed

EXECPLAN-ui-state-persistence.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ After this change, a QuickAdd user will be able to edit complex settings (especi
2525
- [x] (2026-03-02 17:45Z) Implemented Follow-up 5: extracted shared behavior-section helpers for one-page override and file opening controls.
2626
- [x] (2026-03-02 17:48Z) Implemented Follow-up 6: added integration tests for queue + `settingsStore` subscription/dispose lifecycle.
2727
- [x] (2026-03-02 17:53Z) Implemented Follow-up 7: added stable CLI debug command surface (`quickadd:debug`) while retaining namespace compatibility.
28+
- [x] (2026-03-02 21:38Z) Added full simplification feedback backlog from Claude pass (`claude -p "/simplify but don't fix, just give the feedback"`).
29+
- [x] (2026-03-02 21:52Z) Implemented simplification backlog items across builder deduplication, CLI utility reuse, draft-session consistency, queue/preserve-ui refinements, and targeted efficiency cleanups; reran tests/build + Obsidian CLI verification.
2830

2931
## Surprises & Discoveries
3032

@@ -258,6 +260,86 @@ Validation:
258260
- CLI transcript using only stable seam.
259261
- Backward-compatible behavior during deprecation window for global namespace access.
260262

263+
## Simplification Feedback Backlog (Claude Pass, 2026-03-02)
264+
265+
This section captures all feedback items from the Claude simplification pass.
266+
Items are tracked individually even where they overlap previously completed work.
267+
268+
### Code Reuse
269+
270+
- [x] [High] Remove dead base-class methods from
271+
`src/gui/ChoiceBuilder/choiceBuilder.ts`:
272+
`addOnePageOverrideSetting`,
273+
`addOpenFileSetting`,
274+
`addFileOpeningSetting`.
275+
- [x] [High] Deduplicate identical `renderSection` implementations in
276+
`src/gui/ChoiceBuilder/captureChoiceBuilder.ts` and
277+
`src/gui/ChoiceBuilder/templateChoiceBuilder.ts` by moving shared logic
278+
to base class.
279+
- [x] [High] Avoid reimplementing flattening logic in
280+
`src/cli/registerQuickAddCliHandlers.ts`; extend/reuse
281+
`src/utils/choiceUtils.ts:flattenChoices` with optional path segments.
282+
- [x] [High] Replace repeated `instanceof Error ? error.message : String(error)`
283+
handling in CLI with shared `src/utils/errorUtils.ts:toError`.
284+
- [x] [Low] Replace `checkObjectDiff` usage in
285+
`src/gui/AIAssistant/AIAssistantProvidersModal.ts` with
286+
`createDraftSession().isDirty()` where appropriate.
287+
- [x] [Low] Extract a shared save/cancel modal action bar helper used by:
288+
`src/gui/MacroGUIs/OpenFileCommandSettingsModal.ts`,
289+
`src/gui/MacroGUIs/ConditionalCommandSettingsModal.ts`,
290+
`src/gui/MacroGUIs/ConditionalBranchEditorModal.ts`.
291+
292+
### Code Quality
293+
294+
- [x] [Medium] Simplify `flushNow` in
295+
`src/state/settingsPersistenceQueue.ts` by separating timeout, retry, and
296+
loop-wake responsibilities.
297+
- [x] [Medium] Document and/or refactor double-restore behavior in
298+
`src/gui/utils/preserveUiContext.ts:scheduleRestore` to make intent
299+
explicit and controllable.
300+
- [x] [Medium] Consolidate debug API access in `src/main.ts`:
301+
prefer `getDebugApi(): QuickAddDebugApi | null` over multiple pass-through
302+
debug methods and avoid leaking queue internals in public return types.
303+
- [x] [Medium] Migrate `src/gui/AIAssistant/AIAssistantProvidersModal.ts` to
304+
`DraftSession` pattern for consistency with Open File modal editing.
305+
- [x] [Low] Remove dual source of truth for `filterQuery` in
306+
`src/gui/ChoiceView.svelte` (local state vs `uiStore` field).
307+
- [x] [Low] Revisit `scheduledRevisions` stat in
308+
`src/state/settingsPersistenceQueue.ts`; rename or remove to avoid
309+
misleading metrics.
310+
- [x] [Low] Simplify no-op conditional branch in
311+
`src/gui/MacroGUIs/ConditionalCommandSettingsModal.ts:cloneCondition`.
312+
- [x] [Low] Replace stringly-typed debug actions (`"get"`, `"reset"`, `"flush"`)
313+
in `src/cli/registerQuickAddCliHandlers.ts` with typed constants/union.
314+
315+
### Efficiency
316+
317+
- [x] [Medium] Reduce repeated `querySelectorAll` work in
318+
`src/gui/utils/preserveUiContext.ts` by reusing scroll target discovery
319+
across capture + restore passes.
320+
- [x] [Medium] Add caching or memoization for vault-wide scans triggered by
321+
section rerenders in:
322+
`src/gui/ChoiceBuilder/captureChoiceBuilder.ts` and
323+
`src/gui/ChoiceBuilder/templateChoiceBuilder.ts`.
324+
- [x] [Medium] Optimize `createDraftSession().isDirty()` in
325+
`src/state/createDraftSession.ts` for repeated checks (dirty flag and/or
326+
incremental tracking).
327+
- [x] [Medium] Cancel losing timeout in
328+
`src/state/settingsPersistenceQueue.ts:waitForPromiseOrTimeout`.
329+
- [x] [Medium] Reconsider timeout window reset across retries in
330+
`src/state/settingsPersistenceQueue.ts` so max wait does not scale by
331+
retry count under persistent failure.
332+
- [x] [Low] Avoid scheduling persistence writes on semantically unchanged
333+
settings in `src/main.ts` subscriber path.
334+
- [x] [Low] Memoize `prepareFuzzySearch` usage in
335+
`src/gui/ChoiceView.svelte` when inputs are unchanged.
336+
- [x] [Low] Add cleanup/eviction strategy for stale `collapsedChoiceIds` in
337+
`src/gui/stores/uiStore.ts`.
338+
- [x] [Low] Avoid full-vault template scans on every request in
339+
`src/main.ts:getTemplateFiles` (cache/index by folder path).
340+
- [ ] [Low] Revisit `api` getter allocation pattern in `src/main.ts` to avoid
341+
constructing a new `ChoiceExecutor` per access.
342+
261343
## Context and Orientation
262344

263345
This repository is an Obsidian community plugin named QuickAdd. Obsidian plugins persist settings by calling the plugin method `saveData(...)`, which writes JSON into the plugin’s data file (`<vault>/.obsidian/plugins/quickadd/data.json`). In this codebase, settings are held in a Zustand store at `src/settingsStore.ts` and mirrored onto the plugin instance in `src/main.ts`.

src/cli/registerQuickAddCliHandlers.test.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,19 +97,23 @@ function createPlugin(choices: IChoice[]) {
9797
handlers.push({ command, description, flags, handler });
9898
},
9999
),
100-
getDebugStats: vi.fn(() => debugStats),
101-
resetDebugStats: vi.fn(() => true),
102-
flushDebugPersistence: vi.fn(async () => debugStats.persistence),
100+
getDebugApi: vi.fn(() => ({
101+
getStats: () => debugStats,
102+
resetStats: () => undefined,
103+
flushNow: async () => debugStats.persistence,
104+
})),
103105
} as unknown as QuickAdd & {
104106
registerCliHandler: (
105107
command: string,
106108
description: string,
107109
flags: CliFlags | null,
108110
handler: (params: CliData) => string | Promise<string>,
109111
) => void;
110-
getDebugStats: () => unknown;
111-
resetDebugStats: () => boolean;
112-
flushDebugPersistence: () => Promise<unknown>;
112+
getDebugApi: () => {
113+
getStats: () => unknown;
114+
resetStats: () => void;
115+
flushNow: () => Promise<unknown>;
116+
};
113117
};
114118

115119
return { plugin, handlers };

src/cli/registerQuickAddCliHandlers.ts

Lines changed: 67 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import {
77
collectChoiceRequirements,
88
getUnresolvedRequirements,
99
} from "../preflight/collectChoiceRequirements";
10+
import { flattenChoicesWithPath } from "../utils/choiceUtils";
11+
import { toError } from "../utils/errorUtils";
1012
import type IChoice from "../types/choices/IChoice";
11-
import type IMultiChoice from "../types/choices/IMultiChoice";
1213

1314
type ChoiceType = IChoice["type"];
15+
type DebugAction = "get" | "reset" | "flush";
1416

1517
interface CliChoiceSummary {
1618
id: string;
@@ -37,12 +39,14 @@ interface RegisterCliHandlerTarget {
3739
}
3840

3941
interface DebugCliTarget {
40-
getDebugStats?: () => {
41-
persistence: unknown;
42-
ui: unknown;
42+
getDebugApi?: () => {
43+
getStats: () => {
44+
persistence: unknown;
45+
ui: unknown;
46+
};
47+
resetStats: () => void;
48+
flushNow: () => Promise<unknown>;
4349
} | null;
44-
resetDebugStats?: () => boolean;
45-
flushDebugPersistence?: () => Promise<unknown>;
4650
}
4751

4852
const RUN_FLAGS: CliFlags = {
@@ -95,6 +99,12 @@ const DEBUG_FLAGS: CliFlags = {
9599
},
96100
};
97101

102+
const DEBUG_ACTIONS = {
103+
get: "get",
104+
reset: "reset",
105+
flush: "flush",
106+
} as const satisfies Record<DebugAction, DebugAction>;
107+
98108
const RESERVED_RUN_PARAMS = new Set<string>(["choice", "id", "vars", "ui"]);
99109
const RESERVED_CHECK_PARAMS = new Set<string>(["choice", "id", "vars"]);
100110

@@ -128,11 +138,7 @@ function parseVarsJson(value: string): Record<string, unknown> {
128138
try {
129139
parsed = JSON.parse(value);
130140
} catch (error) {
131-
throw new Error(
132-
`Invalid vars JSON: ${
133-
error instanceof Error ? error.message : String(error)
134-
}`,
135-
);
141+
throw toError(error, "Invalid vars JSON");
136142
}
137143

138144
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
@@ -166,34 +172,6 @@ function extractVariables(
166172
return variables;
167173
}
168174

169-
function flattenChoices(
170-
choices: IChoice[],
171-
segments: string[] = [],
172-
): CliChoiceSummary[] {
173-
const flattened: CliChoiceSummary[] = [];
174-
175-
for (const choice of choices) {
176-
const pathSegments = [...segments, choice.name];
177-
const path = pathSegments.join(" / ");
178-
const isMulti = choice.type === "Multi";
179-
flattened.push({
180-
id: choice.id,
181-
name: choice.name,
182-
type: choice.type,
183-
command: choice.command,
184-
path,
185-
runnable: !isMulti,
186-
});
187-
188-
if (isMulti) {
189-
const multiChoice = choice as IMultiChoice;
190-
flattened.push(...flattenChoices(multiChoice.choices, pathSegments));
191-
}
192-
}
193-
194-
return flattened;
195-
}
196-
197175
function resolveChoiceFromParams(plugin: QuickAdd, params: CliData): IChoice {
198176
if (typeof params.id === "string" && params.id.trim().length > 0) {
199177
return plugin.getChoiceById(params.id);
@@ -321,7 +299,7 @@ async function runChoiceHandler(
321299
return serialize({
322300
ok: false,
323301
command,
324-
error: error instanceof Error ? error.message : String(error),
302+
error: toError(error).message,
325303
});
326304
}
327305
}
@@ -340,7 +318,20 @@ function listChoicesHandler(plugin: QuickAdd, params: CliData): string {
340318
});
341319
}
342320

343-
const allChoices = flattenChoices(plugin.settings.choices);
321+
const allChoices = flattenChoicesWithPath(plugin.settings.choices).map(
322+
({ choice, pathSegments }) => {
323+
const isMulti = choice.type === "Multi";
324+
const summary: CliChoiceSummary = {
325+
id: choice.id,
326+
name: choice.name,
327+
type: choice.type,
328+
command: choice.command,
329+
path: pathSegments.join(" / "),
330+
runnable: !isMulti,
331+
};
332+
return summary;
333+
},
334+
);
344335
const choices = allChoices.filter((choice) => {
345336
if (
346337
requestedType &&
@@ -362,7 +353,7 @@ function listChoicesHandler(plugin: QuickAdd, params: CliData): string {
362353
return serialize({
363354
ok: false,
364355
command: CLI_COMMANDS.list,
365-
error: error instanceof Error ? error.message : String(error),
356+
error: toError(error).message,
366357
});
367358
}
368359
}
@@ -415,15 +406,24 @@ async function checkChoiceHandler(
415406
return serialize({
416407
ok: false,
417408
command: CLI_COMMANDS.check,
418-
error: error instanceof Error ? error.message : String(error),
409+
error: toError(error).message,
419410
});
420411
}
421412
}
422413

414+
function parseDebugAction(actionRaw: string | undefined): DebugAction | null {
415+
const normalized = (actionRaw ?? DEBUG_ACTIONS.get).trim().toLowerCase();
416+
if (normalized === DEBUG_ACTIONS.get) return DEBUG_ACTIONS.get;
417+
if (normalized === DEBUG_ACTIONS.reset) return DEBUG_ACTIONS.reset;
418+
if (normalized === DEBUG_ACTIONS.flush) return DEBUG_ACTIONS.flush;
419+
return null;
420+
}
421+
423422
async function debugHandler(plugin: QuickAdd, params: CliData): Promise<string> {
424-
const actionRaw = typeof params.action === "string" ? params.action : "get";
425-
const action = actionRaw.trim().toLowerCase();
423+
const actionRaw = typeof params.action === "string" ? params.action : undefined;
424+
const action = parseDebugAction(actionRaw);
426425
const debugTarget = plugin as unknown as DebugCliTarget;
426+
const debugApi = debugTarget.getDebugApi?.() ?? null;
427427

428428
if (!plugin.settings.devMode) {
429429
return serialize({
@@ -433,48 +433,56 @@ async function debugHandler(plugin: QuickAdd, params: CliData): Promise<string>
433433
});
434434
}
435435

436-
if (action === "get") {
436+
if (!debugApi) {
437+
return serialize({
438+
ok: false,
439+
command: CLI_COMMANDS.debug,
440+
error: "Debug API is unavailable.",
441+
});
442+
}
443+
444+
if (action === DEBUG_ACTIONS.get) {
437445
return serialize({
438446
ok: true,
439447
command: CLI_COMMANDS.debug,
440-
action: "get",
441-
stats: debugTarget.getDebugStats?.() ?? null,
448+
action: DEBUG_ACTIONS.get,
449+
stats: debugApi.getStats(),
442450
});
443451
}
444452

445-
if (action === "reset") {
446-
const reset = debugTarget.resetDebugStats?.() ?? false;
453+
if (action === DEBUG_ACTIONS.reset) {
454+
debugApi.resetStats();
447455
return serialize({
448-
ok: reset,
456+
ok: true,
449457
command: CLI_COMMANDS.debug,
450-
action: "reset",
451-
stats: debugTarget.getDebugStats?.() ?? null,
458+
action: DEBUG_ACTIONS.reset,
459+
stats: debugApi.getStats(),
452460
});
453461
}
454462

455-
if (action === "flush") {
463+
if (action === DEBUG_ACTIONS.flush) {
456464
try {
457-
const persistence = await debugTarget.flushDebugPersistence?.();
465+
const persistence = await debugApi.flushNow();
458466
return serialize({
459467
ok: true,
460468
command: CLI_COMMANDS.debug,
461-
action: "flush",
469+
action: DEBUG_ACTIONS.flush,
462470
persistence: persistence ?? null,
463471
});
464472
} catch (error) {
465473
return serialize({
466474
ok: false,
467475
command: CLI_COMMANDS.debug,
468-
action: "flush",
469-
error: error instanceof Error ? error.message : String(error),
476+
action: DEBUG_ACTIONS.flush,
477+
error: toError(error).message,
470478
});
471479
}
472480
}
473481

474482
return serialize({
475483
ok: false,
476484
command: CLI_COMMANDS.debug,
477-
error: `Invalid action '${actionRaw}'. Use get, reset, or flush.`,
485+
error: `Invalid action '${actionRaw ?? ""}'. Use get, reset, or flush.`,
478486
});
479487
}
480488

0 commit comments

Comments
 (0)