jettypod 4.4.118 → 4.4.121
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.
- package/.env +4 -3
- package/Cargo.lock +6450 -0
- package/Cargo.toml +35 -0
- package/README.md +5 -1
- package/TAURI-MIGRATION-PLAN.md +840 -0
- package/apps/dashboard/app/connect-claude/page.tsx +5 -6
- package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
- package/apps/dashboard/app/demo/gates/page.tsx +43 -45
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +80 -4
- package/apps/dashboard/app/install-claude/page.tsx +4 -6
- package/apps/dashboard/app/login/page.tsx +72 -54
- package/apps/dashboard/app/page.tsx +101 -48
- package/apps/dashboard/app/settings/page.tsx +61 -13
- package/apps/dashboard/app/signup/page.tsx +242 -0
- package/apps/dashboard/app/subscribe/page.tsx +0 -2
- package/apps/dashboard/app/tests/page.tsx +37 -4
- package/apps/dashboard/app/welcome/page.tsx +13 -16
- package/apps/dashboard/app/work/[id]/page.tsx +117 -118
- package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
- package/apps/dashboard/components/AppShell.tsx +92 -85
- package/apps/dashboard/components/CardMenu.tsx +45 -12
- package/apps/dashboard/components/ClaudePanel.tsx +771 -850
- package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
- package/apps/dashboard/components/CopyableId.tsx +3 -4
- package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
- package/apps/dashboard/components/DragContext.tsx +134 -63
- package/apps/dashboard/components/DraggableCard.tsx +3 -5
- package/apps/dashboard/components/DropZone.tsx +6 -7
- package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
- package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
- package/apps/dashboard/components/EditableTitle.tsx +26 -7
- package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
- package/apps/dashboard/components/EpicGroup.tsx +359 -0
- package/apps/dashboard/components/GateCard.tsx +79 -17
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
- package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
- package/apps/dashboard/components/JettyLoader.tsx +37 -0
- package/apps/dashboard/components/KanbanBoard.tsx +368 -958
- package/apps/dashboard/components/KanbanCard.tsx +740 -0
- package/apps/dashboard/components/LazyCard.tsx +62 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
- package/apps/dashboard/components/MainNav.tsx +38 -73
- package/apps/dashboard/components/MessageBlock.tsx +468 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -16
- package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
- package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
- package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
- package/apps/dashboard/components/ReviewFooter.tsx +139 -0
- package/apps/dashboard/components/SessionList.tsx +19 -19
- package/apps/dashboard/components/SubscribeContent.tsx +91 -47
- package/apps/dashboard/components/TestTree.tsx +16 -16
- package/apps/dashboard/components/TipCard.tsx +16 -17
- package/apps/dashboard/components/Toast.tsx +5 -6
- package/apps/dashboard/components/TypeIcon.tsx +55 -0
- package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
- package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
- package/apps/dashboard/components/WorkItemTree.tsx +11 -32
- package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
- package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
- package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
- package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
- package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/components.json +1 -1
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
- package/apps/dashboard/contexts/UsageContext.tsx +87 -32
- package/apps/dashboard/dev.sh +35 -0
- package/apps/dashboard/eslint.config.mjs +9 -9
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/hooks/useWebSocket.ts +138 -83
- package/apps/dashboard/index.html +73 -0
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/data-bridge.ts +722 -0
- package/apps/dashboard/lib/db.ts +69 -1265
- package/apps/dashboard/lib/environment-config.ts +173 -0
- package/apps/dashboard/lib/environment-verification.ts +119 -0
- package/apps/dashboard/lib/kanban-utils.ts +270 -0
- package/apps/dashboard/lib/proof-run.ts +495 -0
- package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- package/apps/dashboard/lib/service-recovery.ts +326 -0
- package/apps/dashboard/lib/session-state-machine.ts +1 -0
- package/apps/dashboard/lib/session-state-utils.ts +0 -164
- package/apps/dashboard/lib/session-stream-manager.ts +308 -134
- package/apps/dashboard/lib/shadows.ts +7 -0
- package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
- package/apps/dashboard/lib/tauri-bridge.ts +102 -0
- package/apps/dashboard/lib/tauri.ts +106 -0
- package/apps/dashboard/lib/utils.ts +6 -0
- package/apps/dashboard/next-env.d.ts +1 -1
- package/apps/dashboard/package.json +21 -32
- package/apps/dashboard/public/bug-icon.png +0 -0
- package/apps/dashboard/public/buoy-icon.png +0 -0
- package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
- package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
- package/apps/dashboard/public/in-flight-seagull.png +0 -0
- package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
- package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
- package/apps/dashboard/public/jettypod_logo.png +0 -0
- package/apps/dashboard/public/pier-icon.png +0 -0
- package/apps/dashboard/public/star-icon.png +0 -0
- package/apps/dashboard/public/wrench-icon.png +0 -0
- package/apps/dashboard/scripts/tauri-build.js +228 -0
- package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
- package/apps/dashboard/scripts/ws-server.js +191 -0
- package/apps/dashboard/src/main.tsx +12 -0
- package/apps/dashboard/src/router.tsx +107 -0
- package/apps/dashboard/src/vite-env.d.ts +1 -0
- package/apps/dashboard/tsconfig.json +7 -12
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
- package/apps/dashboard/vite.config.ts +33 -0
- package/apps/update-server/src/index.ts +228 -80
- package/claude-hooks/global-guardrails.js +14 -13
- package/crates/jettypod-cli/Cargo.toml +19 -0
- package/crates/jettypod-cli/src/commands.rs +1249 -0
- package/crates/jettypod-cli/src/main.rs +595 -0
- package/crates/jettypod-core/Cargo.toml +26 -0
- package/crates/jettypod-core/build.rs +98 -0
- package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
- package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
- package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
- package/crates/jettypod-core/src/auth.rs +294 -0
- package/crates/jettypod-core/src/config.rs +397 -0
- package/crates/jettypod-core/src/db/mod.rs +507 -0
- package/crates/jettypod-core/src/db/recovery.rs +114 -0
- package/crates/jettypod-core/src/db/startup.rs +101 -0
- package/crates/jettypod-core/src/db/validate.rs +149 -0
- package/crates/jettypod-core/src/error.rs +76 -0
- package/crates/jettypod-core/src/git.rs +458 -0
- package/crates/jettypod-core/src/lib.rs +20 -0
- package/crates/jettypod-core/src/sessions.rs +625 -0
- package/crates/jettypod-core/src/skills.rs +556 -0
- package/crates/jettypod-core/src/work.rs +1086 -0
- package/crates/jettypod-core/src/worktree.rs +628 -0
- package/crates/jettypod-core/src/ws.rs +767 -0
- package/cucumber-test.cjs +6 -0
- package/cucumber.js +9 -3
- package/docs/COMMAND_REFERENCE.md +34 -0
- package/hooks/post-checkout +32 -75
- package/hooks/post-merge +111 -10
- package/jest.setup.js +1 -0
- package/jettypod.js +145 -116
- package/lib/bdd-preflight.js +96 -0
- package/lib/chore-taxonomy.js +33 -10
- package/lib/database.js +36 -16
- package/lib/db-watcher.js +1 -1
- package/lib/git-hooks/pre-commit +1 -1
- package/lib/jettypod-backup.js +27 -4
- package/lib/merge-lock.js +111 -253
- package/lib/migrations/027-plan-at-creation-column.js +3 -1
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/030-rejection-round-columns.js +54 -0
- package/lib/migrations/031-session-isolation-index.js +17 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +10 -5
- package/lib/seed-onboarding.js +1 -1
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +144 -19
- package/lib/work-tracking/index.js +148 -27
- package/lib/worktree-diagnostics.js +16 -16
- package/lib/worktree-facade.js +1 -1
- package/lib/worktree-manager.js +8 -8
- package/lib/worktree-reconciler.js +5 -5
- package/package.json +9 -2
- package/scripts/ndjson-to-cucumber-json.js +152 -0
- package/scripts/postinstall.js +25 -0
- package/skills-templates/bug-mode/SKILL.md +79 -20
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +171 -69
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/design-system-selection/SKILL.md +273 -0
- package/skills-templates/epic-planning/SKILL.md +82 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +173 -74
- package/skills-templates/production-mode/SKILL.md +69 -49
- package/skills-templates/request-routing/SKILL.md +4 -4
- package/skills-templates/simple-improvement/SKILL.md +74 -29
- package/skills-templates/speed-mode/SKILL.md +217 -141
- package/skills-templates/stable-mode/SKILL.md +148 -89
- package/apps/dashboard/README.md +0 -36
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
- package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
- package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
- package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
- package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
- package/apps/dashboard/app/api/kanban/route.ts +0 -15
- package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
- package/apps/dashboard/app/api/settings/general/route.ts +0 -21
- package/apps/dashboard/app/api/tests/route.ts +0 -9
- package/apps/dashboard/app/api/tests/run/route.ts +0 -82
- package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
- package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
- package/apps/dashboard/app/api/usage/route.ts +0 -17
- package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
- package/apps/dashboard/app/layout.tsx +0 -43
- package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
- package/apps/dashboard/electron/ipc-handlers.js +0 -1028
- package/apps/dashboard/electron/main.js +0 -2124
- package/apps/dashboard/electron/preload.js +0 -123
- package/apps/dashboard/electron/session-manager.js +0 -141
- package/apps/dashboard/electron-builder.config.js +0 -357
- package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
- package/apps/dashboard/lib/claude-process-manager.ts +0 -492
- package/apps/dashboard/lib/db-bridge.ts +0 -282
- package/apps/dashboard/lib/prototypes.ts +0 -202
- package/apps/dashboard/lib/test-results-db.ts +0 -307
- package/apps/dashboard/lib/tests.ts +0 -282
- package/apps/dashboard/next.config.js +0 -50
- package/apps/dashboard/postcss.config.mjs +0 -7
- package/apps/dashboard/public/file.svg +0 -1
- package/apps/dashboard/public/globe.svg +0 -1
- package/apps/dashboard/public/next.svg +0 -1
- package/apps/dashboard/public/vercel.svg +0 -1
- package/apps/dashboard/public/window.svg +0 -1
- package/apps/dashboard/scripts/download-node.js +0 -104
- package/apps/dashboard/scripts/upload-to-r2.js +0 -89
- package/docs/bdd-guidance.md +0 -390
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Bridge — provides all data operations via Tauri IPC.
|
|
3
|
+
* Low-level invoke/listen/isTauri are in lib/tauri.ts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { invoke } from './tauri';
|
|
7
|
+
import type { TestDashboardData, PrototypeDashboardData } from './db';
|
|
8
|
+
import type { EnvironmentConfig, ContextItem } from './environment-config';
|
|
9
|
+
import { validateCategory, validateFaIcon } from './environment-config';
|
|
10
|
+
|
|
11
|
+
// Work item types matching Tauri IPC response format
|
|
12
|
+
// Note: Rust WorkItemResponse uses #[serde(rename = "type")] so JSON key is "type"
|
|
13
|
+
export interface WorkItemData {
|
|
14
|
+
id: number;
|
|
15
|
+
type: string;
|
|
16
|
+
title: string;
|
|
17
|
+
description: string | null;
|
|
18
|
+
status: string;
|
|
19
|
+
mode: string | null;
|
|
20
|
+
phase: string | null;
|
|
21
|
+
parent_id: number | null;
|
|
22
|
+
branch_name: string | null;
|
|
23
|
+
scenario_file: string | null;
|
|
24
|
+
created_at: string;
|
|
25
|
+
completed_at: string | null;
|
|
26
|
+
ready_for_review: number;
|
|
27
|
+
display_order: number;
|
|
28
|
+
rejection_count: number;
|
|
29
|
+
rejection_reason: string | null;
|
|
30
|
+
rejection_round: number | null;
|
|
31
|
+
rejection_history: string | null;
|
|
32
|
+
needs_discovery: number;
|
|
33
|
+
qa_steps: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DecisionData {
|
|
37
|
+
id: number;
|
|
38
|
+
work_item_id: number;
|
|
39
|
+
work_item_title: string;
|
|
40
|
+
aspect: string;
|
|
41
|
+
decision: string;
|
|
42
|
+
rationale: string | null;
|
|
43
|
+
created_at: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Kanban types (matching what components expect)
|
|
47
|
+
export interface InFlightItem {
|
|
48
|
+
id: number;
|
|
49
|
+
type: string;
|
|
50
|
+
title: string;
|
|
51
|
+
status: string;
|
|
52
|
+
mode: string | null;
|
|
53
|
+
phase: string | null;
|
|
54
|
+
parent_id: number | null;
|
|
55
|
+
parent_title: string | null;
|
|
56
|
+
parent_type: string | null;
|
|
57
|
+
branch_name: string | null;
|
|
58
|
+
ready_for_review: number;
|
|
59
|
+
display_order: number;
|
|
60
|
+
description: string | null;
|
|
61
|
+
rejection_count: number;
|
|
62
|
+
rejection_reason: string | null;
|
|
63
|
+
needs_discovery: number;
|
|
64
|
+
created_at: string;
|
|
65
|
+
completed_at: string | null;
|
|
66
|
+
rejection_round: number | null;
|
|
67
|
+
rejection_history: string | null;
|
|
68
|
+
// Aliases for backward compatibility with components
|
|
69
|
+
epicTitle: string | null;
|
|
70
|
+
epic_id: number | null;
|
|
71
|
+
// Children nested under feature cards (populated by transformToKanbanData)
|
|
72
|
+
chores?: InFlightItem[];
|
|
73
|
+
bugs?: InFlightItem[];
|
|
74
|
+
conversational?: number;
|
|
75
|
+
scenario_file?: string | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface KanbanGroup {
|
|
79
|
+
epicId: number | null;
|
|
80
|
+
epicTitle: string | null;
|
|
81
|
+
items: InFlightItem[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface KanbanData {
|
|
85
|
+
inFlight: InFlightItem[];
|
|
86
|
+
backlog: Map<string, KanbanGroup>;
|
|
87
|
+
done: Map<string, KanbanGroup>;
|
|
88
|
+
/** O(1) item lookup by ID — built once during transform */
|
|
89
|
+
itemMap: Map<number, InFlightItem>;
|
|
90
|
+
/** O(1) status lookup by ID — built once during transform */
|
|
91
|
+
statusMap: Map<number, string>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Project config (matches Rust ProjectConfig serialization)
|
|
95
|
+
export interface ProjectConfig {
|
|
96
|
+
name: string;
|
|
97
|
+
stage: string;
|
|
98
|
+
bundles: string[];
|
|
99
|
+
project_state: string;
|
|
100
|
+
project_discovery: {
|
|
101
|
+
status: string;
|
|
102
|
+
prototypes: unknown[];
|
|
103
|
+
winner: string | null;
|
|
104
|
+
rationale: string | null;
|
|
105
|
+
started_date: string | null;
|
|
106
|
+
completed_date: string | null;
|
|
107
|
+
};
|
|
108
|
+
mainBranch?: string | null;
|
|
109
|
+
[key: string]: unknown;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---- Helper: transform work items into kanban format ----
|
|
113
|
+
|
|
114
|
+
function toInFlightItem(item: WorkItemData, parentMap: Map<number, WorkItemData>): InFlightItem {
|
|
115
|
+
const parent = item.parent_id ? parentMap.get(item.parent_id) : null;
|
|
116
|
+
return {
|
|
117
|
+
id: item.id,
|
|
118
|
+
type: item.type,
|
|
119
|
+
title: item.title,
|
|
120
|
+
status: item.status,
|
|
121
|
+
mode: item.mode,
|
|
122
|
+
phase: item.phase,
|
|
123
|
+
parent_id: item.parent_id,
|
|
124
|
+
parent_title: parent?.title ?? null,
|
|
125
|
+
parent_type: parent?.type ?? null,
|
|
126
|
+
branch_name: item.branch_name,
|
|
127
|
+
ready_for_review: item.ready_for_review,
|
|
128
|
+
display_order: item.display_order,
|
|
129
|
+
description: item.description,
|
|
130
|
+
rejection_count: item.rejection_count,
|
|
131
|
+
rejection_reason: item.rejection_reason,
|
|
132
|
+
needs_discovery: item.needs_discovery,
|
|
133
|
+
created_at: item.created_at,
|
|
134
|
+
completed_at: item.completed_at,
|
|
135
|
+
rejection_round: item.rejection_round ?? null,
|
|
136
|
+
rejection_history: item.rejection_history ?? null,
|
|
137
|
+
// Backward compatibility aliases
|
|
138
|
+
epicTitle: parent?.title ?? null,
|
|
139
|
+
epic_id: item.parent_id,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function groupByEpic(items: InFlightItem[], sort: 'display_order' | 'completed_at_desc' = 'display_order'): Map<string, KanbanGroup> {
|
|
144
|
+
const groups = new Map<string, KanbanGroup>();
|
|
145
|
+
for (const item of items) {
|
|
146
|
+
const key = item.parent_id ? String(item.parent_id) : 'ungrouped';
|
|
147
|
+
const existing = groups.get(key);
|
|
148
|
+
if (existing) {
|
|
149
|
+
existing.items.push(item);
|
|
150
|
+
} else {
|
|
151
|
+
groups.set(key, {
|
|
152
|
+
epicId: item.parent_id,
|
|
153
|
+
epicTitle: item.parent_title,
|
|
154
|
+
items: [item],
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const compareFn = sort === 'completed_at_desc'
|
|
159
|
+
? (a: InFlightItem, b: InFlightItem) => (b.completed_at ?? '').localeCompare(a.completed_at ?? '')
|
|
160
|
+
: (a: InFlightItem, b: InFlightItem) => (a.display_order ?? a.id * 10) - (b.display_order ?? b.id * 10);
|
|
161
|
+
for (const group of groups.values()) {
|
|
162
|
+
group.items.sort(compareFn);
|
|
163
|
+
}
|
|
164
|
+
return groups;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function transformToKanbanData(allItems: WorkItemData[]): KanbanData {
|
|
168
|
+
// Build parent lookup map
|
|
169
|
+
const parentMap = new Map<number, WorkItemData>();
|
|
170
|
+
for (const item of allItems) {
|
|
171
|
+
parentMap.set(item.id, item);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Identify feature IDs so we can nest their children instead of showing them as separate cards
|
|
175
|
+
const featureIds = new Set<number>();
|
|
176
|
+
for (const item of allItems) {
|
|
177
|
+
if (item.type === 'feature') featureIds.add(item.id);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Collect children of features separately — they'll be nested, not shown as cards
|
|
181
|
+
const featureChildren: InFlightItem[] = [];
|
|
182
|
+
|
|
183
|
+
// Separate into categories
|
|
184
|
+
// In-flight: non-epic items with status 'in_progress' or 'blocked'
|
|
185
|
+
// Backlog: non-epic items with status 'backlog' or 'todo'
|
|
186
|
+
// Done: non-epic items with status 'done' or 'cancelled'
|
|
187
|
+
const inFlightItems: InFlightItem[] = [];
|
|
188
|
+
const backlogItems: InFlightItem[] = [];
|
|
189
|
+
const doneItems: InFlightItem[] = [];
|
|
190
|
+
|
|
191
|
+
for (const item of allItems) {
|
|
192
|
+
// Skip epics — they're containers, not kanban cards
|
|
193
|
+
if (item.type === 'epic') continue;
|
|
194
|
+
|
|
195
|
+
const inflightItem = toInFlightItem(item, parentMap);
|
|
196
|
+
|
|
197
|
+
// If this item's parent is a feature, collect it for nesting rather than
|
|
198
|
+
// showing as a separate card
|
|
199
|
+
if (item.parent_id && featureIds.has(item.parent_id)) {
|
|
200
|
+
featureChildren.push(inflightItem);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (item.status === 'in_progress' || item.status === 'blocked') {
|
|
205
|
+
inFlightItems.push(inflightItem);
|
|
206
|
+
} else if (item.status === 'done' || item.status === 'cancelled') {
|
|
207
|
+
doneItems.push(inflightItem);
|
|
208
|
+
} else {
|
|
209
|
+
// backlog, todo
|
|
210
|
+
backlogItems.push(inflightItem);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Sort in-flight by display_order (fall back to id * 10 for null — matches midpoint gap)
|
|
215
|
+
inFlightItems.sort((a, b) => (a.display_order ?? a.id * 10) - (b.display_order ?? b.id * 10));
|
|
216
|
+
|
|
217
|
+
const backlog = groupByEpic(backlogItems);
|
|
218
|
+
const done = groupByEpic(doneItems, 'completed_at_desc');
|
|
219
|
+
|
|
220
|
+
// Build lookup maps once during transform — consumers use these instead of
|
|
221
|
+
// scanning all items with O(N) loops on every access.
|
|
222
|
+
const itemMap = new Map<number, InFlightItem>();
|
|
223
|
+
const statusMap = new Map<number, string>();
|
|
224
|
+
for (const item of inFlightItems) {
|
|
225
|
+
itemMap.set(item.id, item);
|
|
226
|
+
statusMap.set(item.id, item.status);
|
|
227
|
+
}
|
|
228
|
+
for (const group of backlog.values()) {
|
|
229
|
+
for (const item of group.items) {
|
|
230
|
+
itemMap.set(item.id, item);
|
|
231
|
+
statusMap.set(item.id, item.status);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const group of done.values()) {
|
|
235
|
+
for (const item of group.items) {
|
|
236
|
+
itemMap.set(item.id, item);
|
|
237
|
+
statusMap.set(item.id, item.status);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Nest feature children (chores/bugs) onto their parent feature items
|
|
242
|
+
// Also add children to itemMap/statusMap so they're findable
|
|
243
|
+
for (const child of featureChildren) {
|
|
244
|
+
const parent = itemMap.get(child.parent_id!);
|
|
245
|
+
if (parent) {
|
|
246
|
+
if (child.type === 'bug') {
|
|
247
|
+
if (!parent.bugs) parent.bugs = [];
|
|
248
|
+
parent.bugs.push(child);
|
|
249
|
+
} else {
|
|
250
|
+
if (!parent.chores) parent.chores = [];
|
|
251
|
+
parent.chores.push(child);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
itemMap.set(child.id, child);
|
|
255
|
+
statusMap.set(child.id, child.status);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
inFlight: inFlightItems,
|
|
260
|
+
backlog,
|
|
261
|
+
done,
|
|
262
|
+
itemMap,
|
|
263
|
+
statusMap,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---- Project root cache (static value, never changes in a session) ----
|
|
268
|
+
|
|
269
|
+
let projectRootCache: string | null | undefined = undefined; // undefined = not fetched yet
|
|
270
|
+
|
|
271
|
+
// ---- Kanban cache ----
|
|
272
|
+
|
|
273
|
+
let kanbanCache: KanbanData | null = null;
|
|
274
|
+
let lastRawFingerprint: string | null = null;
|
|
275
|
+
let lastKanbanResult: KanbanData | null = null;
|
|
276
|
+
|
|
277
|
+
/** Timestamp of the last local mutation. WebSocket db_change events that arrive
|
|
278
|
+
* within a short window after a local mutation are skipped because the caller
|
|
279
|
+
* already refreshed the data. */
|
|
280
|
+
let lastLocalMutationTs = 0;
|
|
281
|
+
|
|
282
|
+
/** How long (ms) after a local mutation to suppress WebSocket-triggered refetches. */
|
|
283
|
+
const LOCAL_MUTATION_SUPPRESS_MS = 1500;
|
|
284
|
+
|
|
285
|
+
/** Returns true if a local mutation happened recently enough that an incoming
|
|
286
|
+
* WebSocket db_change is almost certainly an echo of our own write. */
|
|
287
|
+
export function isLocalMutationRecent(): boolean {
|
|
288
|
+
return Date.now() - lastLocalMutationTs < LOCAL_MUTATION_SUPPRESS_MS;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Mark that a local mutation is in flight — suppresses WS-triggered refetches
|
|
292
|
+
* without clearing the cache. Call this BEFORE the IPC so the suppression
|
|
293
|
+
* window covers the entire round-trip. invalidateKanbanCache() extends the
|
|
294
|
+
* window after the IPC completes. */
|
|
295
|
+
export function markLocalMutation(): void {
|
|
296
|
+
lastLocalMutationTs = Date.now();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Clear the kanban cache so the next getKanbanData() fetches fresh from DB. */
|
|
300
|
+
export function invalidateKanbanCache(): void {
|
|
301
|
+
kanbanCache = null;
|
|
302
|
+
lastLocalMutationTs = Date.now();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Apply a field-level update to the cached kanban data without full invalidation.
|
|
306
|
+
* Returns true if the item was found and updated, false otherwise. */
|
|
307
|
+
function applyCacheUpdate(id: number, updates: Partial<Pick<InFlightItem, 'title' | 'description'>>): boolean {
|
|
308
|
+
if (!kanbanCache) return false;
|
|
309
|
+
const item = kanbanCache.itemMap.get(id);
|
|
310
|
+
if (!item) return false;
|
|
311
|
+
|
|
312
|
+
// Apply updates in place
|
|
313
|
+
if (updates.title !== undefined) {
|
|
314
|
+
item.title = updates.title;
|
|
315
|
+
// Update fingerprint to match new state
|
|
316
|
+
if (lastRawFingerprint) {
|
|
317
|
+
lastRawFingerprint = lastRawFingerprint.replace(
|
|
318
|
+
new RegExp(`${id}:${item.status}:${item.display_order}:[^;]*;`),
|
|
319
|
+
`${id}:${item.status}:${item.display_order}:${updates.title};`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (updates.description !== undefined) {
|
|
324
|
+
item.description = updates.description;
|
|
325
|
+
}
|
|
326
|
+
lastLocalMutationTs = Date.now();
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---- Delta update support ----
|
|
331
|
+
|
|
332
|
+
/** Fetch a single work item by ID and patch it into the kanban cache.
|
|
333
|
+
* Returns the updated KanbanData if the cache was patched, or null if a full
|
|
334
|
+
* refetch is needed (item not in cache, status changed requiring column move,
|
|
335
|
+
* or cache is cold). */
|
|
336
|
+
export async function patchKanbanItem(rowid: number): Promise<KanbanData | null> {
|
|
337
|
+
if (!kanbanCache) return null;
|
|
338
|
+
|
|
339
|
+
const rawItem = await invoke<WorkItemData | null>('db_get_work_item', { id: rowid });
|
|
340
|
+
if (!rawItem) return null;
|
|
341
|
+
|
|
342
|
+
// Skip epics — they're containers, not kanban cards
|
|
343
|
+
if (rawItem.type === 'epic') return kanbanCache;
|
|
344
|
+
|
|
345
|
+
const existing = kanbanCache.itemMap.get(rowid);
|
|
346
|
+
if (!existing) {
|
|
347
|
+
// Item not in cache (new item or cache stale) — caller should full refetch
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// If status changed, the item needs to move between columns — that's complex
|
|
352
|
+
// enough that a full refetch is cleaner and these are infrequent events
|
|
353
|
+
if (rawItem.status !== existing.status) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Build parent lookup from cache for the toInFlightItem-style transform
|
|
358
|
+
const parentTitle = existing.parent_title;
|
|
359
|
+
const parentType = existing.parent_type;
|
|
360
|
+
|
|
361
|
+
// Patch the item in place — same column, just updated fields
|
|
362
|
+
const patched: InFlightItem = {
|
|
363
|
+
...existing,
|
|
364
|
+
title: rawItem.title,
|
|
365
|
+
description: rawItem.description,
|
|
366
|
+
mode: rawItem.mode,
|
|
367
|
+
phase: rawItem.phase,
|
|
368
|
+
branch_name: rawItem.branch_name,
|
|
369
|
+
ready_for_review: rawItem.ready_for_review,
|
|
370
|
+
display_order: rawItem.display_order,
|
|
371
|
+
rejection_count: rawItem.rejection_count,
|
|
372
|
+
rejection_reason: rawItem.rejection_reason,
|
|
373
|
+
needs_discovery: rawItem.needs_discovery,
|
|
374
|
+
scenario_file: rawItem.scenario_file,
|
|
375
|
+
completed_at: rawItem.completed_at,
|
|
376
|
+
parent_title: parentTitle,
|
|
377
|
+
parent_type: parentType,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// Update fingerprint to match new state
|
|
381
|
+
if (lastRawFingerprint) {
|
|
382
|
+
const oldPattern = `${rowid}:${existing.status}:${existing.display_order}:${existing.title};`;
|
|
383
|
+
const newPattern = `${rowid}:${patched.status}:${patched.display_order}:${patched.title};`;
|
|
384
|
+
lastRawFingerprint = lastRawFingerprint.replace(oldPattern, newPattern);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Replace item in all data structures
|
|
388
|
+
const newItemMap = new Map(kanbanCache.itemMap);
|
|
389
|
+
newItemMap.set(rowid, patched);
|
|
390
|
+
|
|
391
|
+
const statusColumn = existing.status === 'in_progress' || existing.status === 'blocked'
|
|
392
|
+
? 'inFlight' as const
|
|
393
|
+
: existing.status === 'done' || existing.status === 'cancelled'
|
|
394
|
+
? 'done' as const
|
|
395
|
+
: 'backlog' as const;
|
|
396
|
+
|
|
397
|
+
let newInFlight = kanbanCache.inFlight;
|
|
398
|
+
let newBacklog = kanbanCache.backlog;
|
|
399
|
+
let newDone = kanbanCache.done;
|
|
400
|
+
|
|
401
|
+
if (statusColumn === 'inFlight') {
|
|
402
|
+
newInFlight = kanbanCache.inFlight.map(i => i.id === rowid ? patched : i);
|
|
403
|
+
} else {
|
|
404
|
+
const groups = statusColumn === 'backlog' ? kanbanCache.backlog : kanbanCache.done;
|
|
405
|
+
const newGroups = new Map<string, KanbanGroup>();
|
|
406
|
+
for (const [key, group] of groups) {
|
|
407
|
+
if (group.items.some(i => i.id === rowid)) {
|
|
408
|
+
newGroups.set(key, { ...group, items: group.items.map(i => i.id === rowid ? patched : i) });
|
|
409
|
+
} else {
|
|
410
|
+
newGroups.set(key, group);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (statusColumn === 'backlog') newBacklog = newGroups;
|
|
414
|
+
else newDone = newGroups;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const result: KanbanData = {
|
|
418
|
+
inFlight: newInFlight,
|
|
419
|
+
backlog: newBacklog,
|
|
420
|
+
done: newDone,
|
|
421
|
+
itemMap: newItemMap,
|
|
422
|
+
statusMap: kanbanCache.statusMap,
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
kanbanCache = result;
|
|
426
|
+
lastKanbanResult = result;
|
|
427
|
+
return result;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ---- Page data prefetch cache ----
|
|
431
|
+
// Caches promises with a short TTL so hover-triggered fetches are reused by the
|
|
432
|
+
// page component mount. After 10s the cache expires and the next access fetches fresh.
|
|
433
|
+
|
|
434
|
+
const PREFETCH_TTL = 10_000;
|
|
435
|
+
const pageCache = new Map<string, { promise: Promise<any>; ts: number }>();
|
|
436
|
+
|
|
437
|
+
function getCachedOrFetch<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
|
|
438
|
+
const cached = pageCache.get(key);
|
|
439
|
+
if (cached && Date.now() - cached.ts < PREFETCH_TTL) return cached.promise as Promise<T>;
|
|
440
|
+
const promise = fetcher();
|
|
441
|
+
pageCache.set(key, { promise, ts: Date.now() });
|
|
442
|
+
return promise;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export interface ContextDocument {
|
|
446
|
+
type: 'file' | 'text';
|
|
447
|
+
name: string;
|
|
448
|
+
content?: string;
|
|
449
|
+
path?: string;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export interface SettingsPrefetchData {
|
|
453
|
+
files: string[];
|
|
454
|
+
selected: string | null;
|
|
455
|
+
branch: string;
|
|
456
|
+
claudeModel: string | null;
|
|
457
|
+
designSystemDir: string | null;
|
|
458
|
+
contextDocuments: ContextDocument[];
|
|
459
|
+
environmentConfig: EnvironmentConfig | null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export const prefetch = {
|
|
463
|
+
backlog(): Promise<KanbanData> {
|
|
464
|
+
return getCachedOrFetch('backlog', () => dataBridge.getKanbanData());
|
|
465
|
+
},
|
|
466
|
+
tests(): Promise<TestDashboardData> {
|
|
467
|
+
return getCachedOrFetch('tests', () => dataBridge.getTestDashboardData());
|
|
468
|
+
},
|
|
469
|
+
prototypes(): Promise<PrototypeDashboardData> {
|
|
470
|
+
return getCachedOrFetch('prototypes', () => dataBridge.getPrototypeDashboardData());
|
|
471
|
+
},
|
|
472
|
+
workItem(id: number): Promise<{ item: WorkItemData | null; children: WorkItemData[]; decisions: DecisionData[] }> {
|
|
473
|
+
return getCachedOrFetch(`workItem:${id}`, async () => {
|
|
474
|
+
const [item, children, decisions] = await Promise.all([
|
|
475
|
+
dataBridge.getWorkItem(id),
|
|
476
|
+
dataBridge.getChildren(id),
|
|
477
|
+
dataBridge.getDecisions(id),
|
|
478
|
+
]);
|
|
479
|
+
return { item, children, decisions };
|
|
480
|
+
});
|
|
481
|
+
},
|
|
482
|
+
settings(): Promise<SettingsPrefetchData> {
|
|
483
|
+
return getCachedOrFetch('settings', async () => {
|
|
484
|
+
const [files, selected, branch, claudeModel, designSystemDir, contextDocuments, environmentConfig] = await Promise.all([
|
|
485
|
+
dataBridge.discoverEnvFiles(),
|
|
486
|
+
dataBridge.getSelectedEnvFile(),
|
|
487
|
+
dataBridge.getMainBranch(),
|
|
488
|
+
dataBridge.getClaudeModel(),
|
|
489
|
+
dataBridge.getDesignSystemDir(),
|
|
490
|
+
dataBridge.getContextDocuments(),
|
|
491
|
+
dataBridge.getEnvironmentConfig(),
|
|
492
|
+
]);
|
|
493
|
+
return { files, selected, branch, claudeModel, designSystemDir, contextDocuments, environmentConfig };
|
|
494
|
+
});
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// ---- Public API ----
|
|
499
|
+
|
|
500
|
+
export const dataBridge = {
|
|
501
|
+
// Kanban / work items
|
|
502
|
+
async getKanbanData(): Promise<KanbanData> {
|
|
503
|
+
if (kanbanCache) return kanbanCache;
|
|
504
|
+
// Fetch all active items + the 25 most recent done/cancelled for the Done
|
|
505
|
+
// column. This avoids loading 1,000+ completed items while still showing
|
|
506
|
+
// recent completions on the board.
|
|
507
|
+
const items = await invoke<WorkItemData[]>('db_get_tree', {
|
|
508
|
+
includeCompleted: true,
|
|
509
|
+
completedLimit: 25,
|
|
510
|
+
});
|
|
511
|
+
// Lightweight fingerprint — only hash id:status:order:title per item instead of
|
|
512
|
+
// full JSON.stringify. O(N) string concat is much cheaper than O(N) serialization.
|
|
513
|
+
let fingerprint = '';
|
|
514
|
+
for (const item of items) {
|
|
515
|
+
fingerprint += `${item.id}:${item.status}:${item.display_order}:${item.title};`;
|
|
516
|
+
}
|
|
517
|
+
if (fingerprint === lastRawFingerprint && lastKanbanResult) {
|
|
518
|
+
kanbanCache = lastKanbanResult;
|
|
519
|
+
return kanbanCache;
|
|
520
|
+
}
|
|
521
|
+
lastRawFingerprint = fingerprint;
|
|
522
|
+
kanbanCache = transformToKanbanData(items);
|
|
523
|
+
lastKanbanResult = kanbanCache;
|
|
524
|
+
return kanbanCache;
|
|
525
|
+
},
|
|
526
|
+
|
|
527
|
+
async getWorkItem(id: number): Promise<WorkItemData | null> {
|
|
528
|
+
try {
|
|
529
|
+
return await invoke<WorkItemData>('db_get_work_item', { id });
|
|
530
|
+
} catch {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
async getChildren(parentId: number): Promise<WorkItemData[]> {
|
|
536
|
+
return invoke<WorkItemData[]>('db_get_children', { parentId });
|
|
537
|
+
},
|
|
538
|
+
|
|
539
|
+
async getDecisions(workItemId: number): Promise<DecisionData[]> {
|
|
540
|
+
return invoke<DecisionData[]>('db_get_decisions', { workItemId });
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
async getDecision(id: number): Promise<DecisionData | null> {
|
|
544
|
+
try {
|
|
545
|
+
return await invoke<DecisionData>('db_get_decision', { id });
|
|
546
|
+
} catch {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
},
|
|
550
|
+
|
|
551
|
+
// Mutations (all invalidate kanban cache)
|
|
552
|
+
async updateStatus(id: number, status: string, rejectionReason?: string): Promise<boolean> {
|
|
553
|
+
const result = await invoke<boolean>('db_update_work_item_status', { id, status });
|
|
554
|
+
if (rejectionReason && status === 'in_progress') {
|
|
555
|
+
await invoke<boolean>('db_increment_rejection', { id, reason: rejectionReason });
|
|
556
|
+
}
|
|
557
|
+
invalidateKanbanCache();
|
|
558
|
+
return result;
|
|
559
|
+
},
|
|
560
|
+
|
|
561
|
+
async updateTitle(id: number, title: string): Promise<boolean> {
|
|
562
|
+
const result = await invoke<boolean>('db_update_work_item_title', { id, title });
|
|
563
|
+
// Optimistic: update cached item in place instead of full invalidation
|
|
564
|
+
if (!applyCacheUpdate(id, { title })) invalidateKanbanCache();
|
|
565
|
+
return result;
|
|
566
|
+
},
|
|
567
|
+
|
|
568
|
+
async updateDescription(id: number, description: string): Promise<boolean> {
|
|
569
|
+
const result = await invoke<boolean>('db_update_work_item_description', { id, description });
|
|
570
|
+
// Optimistic: update cached item in place instead of full invalidation
|
|
571
|
+
if (!applyCacheUpdate(id, { description })) invalidateKanbanCache();
|
|
572
|
+
return result;
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
async setQaSteps(id: number, qaSteps: string): Promise<boolean> {
|
|
576
|
+
return invoke<boolean>('db_set_work_item_qa_steps', { id, qaSteps });
|
|
577
|
+
},
|
|
578
|
+
|
|
579
|
+
async updateDisplayOrders(orders: [number, number][]): Promise<boolean> {
|
|
580
|
+
const result = await invoke<boolean>('db_update_display_orders', { orders });
|
|
581
|
+
invalidateKanbanCache();
|
|
582
|
+
return result;
|
|
583
|
+
},
|
|
584
|
+
|
|
585
|
+
// Project (cached — project root never changes in a session)
|
|
586
|
+
async hasProject(): Promise<boolean> {
|
|
587
|
+
const root = await this.getProjectRoot();
|
|
588
|
+
return root !== null;
|
|
589
|
+
},
|
|
590
|
+
|
|
591
|
+
async getProjectRoot(): Promise<string | null> {
|
|
592
|
+
if (projectRootCache !== undefined) return projectRootCache;
|
|
593
|
+
projectRootCache = await invoke<string | null>('db_get_project_root');
|
|
594
|
+
return projectRootCache;
|
|
595
|
+
},
|
|
596
|
+
|
|
597
|
+
// Settings
|
|
598
|
+
async getEnvVars(file: string): Promise<Array<{ key: string; value: string }>> {
|
|
599
|
+
return invoke<Array<{ key: string; value: string }>>('db_get_env_vars', { file });
|
|
600
|
+
},
|
|
601
|
+
|
|
602
|
+
async discoverEnvFiles(): Promise<string[]> {
|
|
603
|
+
return invoke<string[]>('db_discover_env_files');
|
|
604
|
+
},
|
|
605
|
+
|
|
606
|
+
async getSelectedEnvFile(): Promise<string | null> {
|
|
607
|
+
return invoke<string | null>('db_get_selected_env_file');
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
async getMainBranch(): Promise<string> {
|
|
611
|
+
return invoke<string>('db_get_main_branch');
|
|
612
|
+
},
|
|
613
|
+
|
|
614
|
+
async getClaudeModel(): Promise<string | null> {
|
|
615
|
+
return invoke<string | null>('db_get_claude_model');
|
|
616
|
+
},
|
|
617
|
+
|
|
618
|
+
async setClaudeModel(model: string): Promise<boolean> {
|
|
619
|
+
return invoke<boolean>('db_set_claude_model', { model });
|
|
620
|
+
},
|
|
621
|
+
|
|
622
|
+
async resetClaudeModel(): Promise<boolean> {
|
|
623
|
+
return invoke<boolean>('db_reset_claude_model');
|
|
624
|
+
},
|
|
625
|
+
|
|
626
|
+
async getDesignSystemDir(): Promise<string | null> {
|
|
627
|
+
return invoke<string | null>('db_get_design_system_dir');
|
|
628
|
+
},
|
|
629
|
+
|
|
630
|
+
async setDesignSystemDir(dir: string): Promise<boolean> {
|
|
631
|
+
return invoke<boolean>('db_set_design_system_dir', { dir });
|
|
632
|
+
},
|
|
633
|
+
|
|
634
|
+
async resetDesignSystemDir(): Promise<boolean> {
|
|
635
|
+
return invoke<boolean>('db_reset_design_system_dir');
|
|
636
|
+
},
|
|
637
|
+
|
|
638
|
+
// Context Documents
|
|
639
|
+
async getContextDocuments(): Promise<ContextDocument[]> {
|
|
640
|
+
return invoke<ContextDocument[]>('db_get_context_documents');
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
async addContextDocument(doc: ContextDocument): Promise<boolean> {
|
|
644
|
+
return invoke<boolean>('db_add_context_document', { doc });
|
|
645
|
+
},
|
|
646
|
+
|
|
647
|
+
async updateContextDocument(index: number, doc: ContextDocument): Promise<boolean> {
|
|
648
|
+
return invoke<boolean>('db_update_context_document', { index, doc });
|
|
649
|
+
},
|
|
650
|
+
|
|
651
|
+
async removeContextDocument(index: number): Promise<boolean> {
|
|
652
|
+
return invoke<boolean>('db_remove_context_document', { index });
|
|
653
|
+
},
|
|
654
|
+
|
|
655
|
+
// Environment Config
|
|
656
|
+
async getEnvironmentConfig(): Promise<EnvironmentConfig | null> {
|
|
657
|
+
try {
|
|
658
|
+
const fullConfig = await invoke<Record<string, unknown>>('db_get_config');
|
|
659
|
+
const env = fullConfig?.environment as Record<string, unknown> | undefined;
|
|
660
|
+
if (!env || typeof env !== 'object' || !Array.isArray(env.services) || env.services.length === 0) {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
const contextItems = Array.isArray(env.contextItems)
|
|
664
|
+
? (env.contextItems as ContextItem[]).map(item => ({
|
|
665
|
+
name: item.name,
|
|
666
|
+
category: item.category,
|
|
667
|
+
faIcon: item.faIcon,
|
|
668
|
+
command: item.command,
|
|
669
|
+
connectionUrl: item.connectionUrl,
|
|
670
|
+
}))
|
|
671
|
+
: [];
|
|
672
|
+
|
|
673
|
+
return {
|
|
674
|
+
services: (env.services as EnvironmentConfig['services']).map((svc, i) => ({
|
|
675
|
+
name: svc.name,
|
|
676
|
+
command: svc.command,
|
|
677
|
+
port: svc.port,
|
|
678
|
+
healthCheck: svc.healthCheck || 'http',
|
|
679
|
+
readyPattern: svc.readyPattern,
|
|
680
|
+
optional: svc.optional ?? false,
|
|
681
|
+
order: svc.order ?? i + 1,
|
|
682
|
+
category: validateCategory(svc.category),
|
|
683
|
+
faIcon: validateFaIcon(svc.faIcon),
|
|
684
|
+
})),
|
|
685
|
+
contextItems,
|
|
686
|
+
readyWhen: (env.readyWhen as EnvironmentConfig['readyWhen']) || 'required',
|
|
687
|
+
teardownOnClose: env.teardownOnClose !== false,
|
|
688
|
+
};
|
|
689
|
+
} catch {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
|
|
694
|
+
async setEnvironmentConfig(config: EnvironmentConfig): Promise<boolean> {
|
|
695
|
+
try {
|
|
696
|
+
const result = await invoke<boolean>('db_set_environment_config', {
|
|
697
|
+
environment: config,
|
|
698
|
+
});
|
|
699
|
+
if (result) pageCache.delete('settings');
|
|
700
|
+
return result;
|
|
701
|
+
} catch {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
|
|
706
|
+
// Tests & Prototypes
|
|
707
|
+
async getTestDashboardData(): Promise<TestDashboardData> {
|
|
708
|
+
return invoke<TestDashboardData>('db_get_test_dashboard_data');
|
|
709
|
+
},
|
|
710
|
+
|
|
711
|
+
async ingestTestResults(): Promise<number> {
|
|
712
|
+
return invoke<number>('ingest_test_results');
|
|
713
|
+
},
|
|
714
|
+
|
|
715
|
+
async getProjectRoot(): Promise<string | null> {
|
|
716
|
+
return invoke<string | null>('db_get_project_root');
|
|
717
|
+
},
|
|
718
|
+
|
|
719
|
+
async getPrototypeDashboardData(): Promise<PrototypeDashboardData> {
|
|
720
|
+
return invoke<PrototypeDashboardData>('db_get_prototype_dashboard_data');
|
|
721
|
+
},
|
|
722
|
+
};
|