Skip to content

Commit c0229db

Browse files
committed
refactor: split monolithic modules into organized sub-modules
- Reorganize IPC layer into procedures and core abstractions - Split ACP session into config, elicitation, errors, paths, and terminal - Extract base agent runtime and utilities into sub-modules - Factor provider icon logic into createProviderIcon utility - Refactor ThreadRuntimeRequestPanel into organized components - Enhance chat pane with QuestionAnswer and tool payload handling - Split runtime thread session into helpers, event routing, env, and registry - Add question/answer event handling across agent adapters - Improve canonical mapping state management for SDK providers - Add interactive testing skill for UI automation
1 parent e474422 commit c0229db

120 files changed

Lines changed: 10677 additions & 7232 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/interactive-testing/SKILL.md

Lines changed: 335 additions & 0 deletions
Large diffs are not rendered by default.

.claude/skills/interactive-testing/SKILL.md

Lines changed: 335 additions & 0 deletions
Large diffs are not rendered by default.

lint-staged.config.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const oxBatchSize = 40;
2+
3+
export default {
4+
"*.{js,jsx,ts,tsx,mjs,cjs}": (filenames) => {
5+
const commands = [];
6+
for (let i = 0; i < filenames.length; i += oxBatchSize) {
7+
const batch = filenames
8+
.slice(i, i + oxBatchSize)
9+
.map((f) => `"${f}"`)
10+
.join(" ");
11+
commands.push(`oxlint --type-aware --fix --deny-warnings ${batch}`);
12+
commands.push(`oxfmt --write ${batch}`);
13+
}
14+
return commands;
15+
},
16+
"*.{json,jsonc,css,md,html,yml,toml}": (filenames) => {
17+
const commands = [];
18+
for (let i = 0; i < filenames.length; i += oxBatchSize) {
19+
const batch = filenames
20+
.slice(i, i + oxBatchSize)
21+
.map((f) => `"${f}"`)
22+
.join(" ");
23+
commands.push(`oxfmt --write ${batch}`);
24+
}
25+
return commands;
26+
},
27+
};

package.json

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,6 @@
133133
"vitest": "^4.1.5",
134134
"wait-on": "^9.0.4"
135135
},
136-
"lint-staged": {
137-
"*.{js,jsx,ts,tsx,mjs,cjs}": [
138-
"oxlint --type-aware --fix --deny-warnings",
139-
"oxfmt --write"
140-
],
141-
"*.{json,jsonc,css,md,html,yml,toml}": "oxfmt --write"
142-
},
143136
"engines": {
144137
"node": ">=24.10.0"
145138
},

src/main/db.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,10 +540,19 @@ export function dbGetThreadRuntimeItems(threadId: string): PersistedRuntimeItem[
540540
export function dbReplaceThreadRuntimeItems(threadId: string, items: PersistedRuntimeItem[]): void {
541541
if (!_sqlite) throw new Error("Database not initialized");
542542
_sqlite.transaction(() => {
543+
if (!threadExistsInSqlite(_sqlite!, threadId)) return;
543544
replaceThreadRuntimeItemsInSqlite(_sqlite!, threadId, items);
544545
})();
545546
}
546547

548+
function threadExistsInSqlite(sqlite: InstanceType<typeof Database>, threadId: string): boolean {
549+
return (
550+
(sqlite.prepare("SELECT 1 FROM threads WHERE id = ?").get(threadId) as
551+
| { "1": number }
552+
| undefined) !== undefined
553+
);
554+
}
555+
547556
function replaceThreadRuntimeItemsInSqlite(
548557
sqlite: InstanceType<typeof Database>,
549558
threadId: string,
@@ -627,6 +636,7 @@ export function dbReplaceThreadCompletedTurns(
627636
): void {
628637
if (!_sqlite) throw new Error("Database not initialized");
629638
_sqlite.transaction(() => {
639+
if (!threadExistsInSqlite(_sqlite!, threadId)) return;
630640
replaceThreadCompletedTurnsInSqlite(_sqlite!, threadId, turns);
631641
})();
632642
}
@@ -639,6 +649,7 @@ export function dbReplaceThreadRuntimeSnapshot(
639649
): void {
640650
if (!_sqlite) throw new Error("Database not initialized");
641651
_sqlite.transaction(() => {
652+
if (!threadExistsInSqlite(_sqlite!, threadId)) return;
642653
replaceThreadRuntimeItemsInSqlite(_sqlite!, threadId, items);
643654
replaceThreadCompletedTurnsInSqlite(_sqlite!, threadId, turns);
644655
if (contextUsage !== undefined) {

src/main/main.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ import { readOrCreateSafeStorageSecretKey } from "./secretStorageKey";
2727
const isDev = Boolean(process.env.VITE_DEV_SERVER_URL);
2828
const channel = resolveLightcodeChannel();
2929

30+
if (process.env.LIGHTCODE_CDP_PORT) {
31+
app.commandLine.appendSwitch("remote-debugging-port", process.env.LIGHTCODE_CDP_PORT);
32+
}
33+
3034
if (isDev) {
3135
app.setPath("userData", join(app.getPath("userData"), "Dev"));
3236
}
@@ -96,8 +100,10 @@ if (!hasSingleInstanceLock) {
96100
app.whenReady().then(async () => {
97101
installLocalFileProtocolHandler();
98102

103+
const baseDirOverride = process.env.LIGHTCODE_BASE_DIR;
99104
lightcodePaths = prepareLightcodeDataRoot(
100-
isDev ? join(homedir(), ".lightcode-dev") : resolveLightcodeBaseDir(channel),
105+
baseDirOverride ??
106+
(isDev ? join(homedir(), ".lightcode-dev") : resolveLightcodeBaseDir(channel)),
101107
);
102108
let jobObjectReady: Promise<void> = Promise.resolve();
103109
if (process.platform === "win32") {

src/renderer/components/common/ProviderModelMenu/ProviderModelMenu.test.tsx

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -447,15 +447,18 @@ describe("ProviderModelMenu", () => {
447447

448448
it("shows shortcut sub-provider labels before provider icons", async () => {
449449
useSharedSettings.setState({
450-
favoriteModels: [{ agentKind: "opencode", modelId: "github-copilot/model-1" }],
451-
recentModels: [{ agentKind: "opencode", modelId: "openai/model-1" }],
450+
favoriteModels: [
451+
{ agentKind: "opencode", modelId: "github-copilot/model-1", presentationMode: "gui" },
452+
],
453+
recentModels: [{ agentKind: "opencode", modelId: "openai/model-1", presentationMode: "gui" }],
452454
});
453455

454456
render(
455457
<ProviderModelMenu
456458
providers={[makeSubProviderBackedProvider(), makeNamedProvider("claude", "Claude", 3)]}
457459
currentAgentKind="opencode"
458460
currentModel="github-copilot/model-2"
461+
presentationMode="gui"
459462
onChange={vi.fn<(next: { agentKind: string; model: string }) => void>()}
460463
/>,
461464
);
@@ -484,9 +487,43 @@ describe("ProviderModelMenu", () => {
484487
await assertShortcutRailOrder("OpenAI Model 1", "OpenAI");
485488
});
486489

490+
it("keeps shortcut favorites and recents scoped to the current presentation mode", async () => {
491+
useSharedSettings.setState({
492+
favoriteModels: [
493+
{ agentKind: "codex", modelId: "gui-fav", presentationMode: "gui" },
494+
{ agentKind: "codex", modelId: "terminal-fav", presentationMode: "terminal" },
495+
],
496+
recentModels: [
497+
{ agentKind: "codex", modelId: "gui-recent", presentationMode: "gui" },
498+
{ agentKind: "codex", modelId: "terminal-recent", presentationMode: "terminal" },
499+
],
500+
});
501+
502+
render(
503+
<ProviderModelMenu
504+
providers={[
505+
makeNamedProvider("codex", "Codex", 1),
506+
makeNamedProvider("claude", "Claude", 1),
507+
]}
508+
currentAgentKind="codex"
509+
currentModel="model-1"
510+
presentationMode="gui"
511+
onChange={vi.fn<(next: { agentKind: string; model: string }) => void>()}
512+
/>,
513+
);
514+
515+
fireEvent.click(screen.getByRole("button", { name: "Select model" }));
516+
517+
const listbox = await screen.findByRole("listbox", { name: "Models" });
518+
expect(within(listbox).getByText("Gui Fav")).toBeInTheDocument();
519+
expect(within(listbox).getByText("Gui Recent")).toBeInTheDocument();
520+
expect(within(listbox).queryByText("Terminal Fav")).not.toBeInTheDocument();
521+
expect(within(listbox).queryByText("Terminal Recent")).not.toBeInTheDocument();
522+
});
523+
487524
it("does not duplicate favorites into a separate section when only one provider is visible", async () => {
488525
useSharedSettings.setState({
489-
favoriteModels: [{ agentKind: "codex", modelId: "model-2" }],
526+
favoriteModels: [{ agentKind: "codex", modelId: "model-2", presentationMode: "gui" }],
490527
recentModels: [],
491528
});
492529

@@ -495,6 +532,7 @@ describe("ProviderModelMenu", () => {
495532
providers={[makeProvider(3)]}
496533
currentAgentKind="codex"
497534
currentModel="model-1"
535+
presentationMode="gui"
498536
onChange={vi.fn<(next: { agentKind: string; model: string }) => void>()}
499537
/>,
500538
);
@@ -511,7 +549,7 @@ describe("ProviderModelMenu", () => {
511549

512550
it("hoists the selected favorite to the top of the single-provider list when reopened", async () => {
513551
useSharedSettings.setState({
514-
favoriteModels: [{ agentKind: "codex", modelId: "model-500" }],
552+
favoriteModels: [{ agentKind: "codex", modelId: "model-500", presentationMode: "gui" }],
515553
recentModels: [],
516554
});
517555

@@ -520,6 +558,7 @@ describe("ProviderModelMenu", () => {
520558
providers={[makeProvider(500)]}
521559
currentAgentKind="codex"
522560
currentModel="model-500"
561+
presentationMode="gui"
523562
onChange={vi.fn<(next: { agentKind: string; model: string }) => void>()}
524563
/>,
525564
);

src/renderer/components/common/ProviderModelMenu/ProviderModelMenu.tsx

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Check, ChevronDown, Search, Star } from "lucide-react";
1111
import { Popover, Tooltip } from "@heroui/react";
1212
import { ProviderIcon } from "@/renderer/components/providers/ProviderIcon";
1313
import { useSharedSettings } from "@/renderer/state/sharedSettingsStore";
14+
import type { ThreadPresentationMode } from "@/shared/contracts";
1415
import { migrateCursorBaseId, parseCursorModelId } from "@/shared/cursorModelId";
1516
import { Button } from "../Button";
1617
import {
@@ -26,10 +27,8 @@ export type { ProviderModelMenuProvider };
2627
const MODEL_MENU_ROW_HEIGHT = 28;
2728
const MODEL_MENU_PROVIDER_HEADER_BOTTOM_GAP = 4;
2829
const MODEL_MENU_MAX_HEIGHT = 288;
29-
const MODEL_MENU_LISTBOX_PADDING_TOP = 6;
3030
const MODEL_MENU_LISTBOX_PADDING_BOTTOM = 6;
31-
const MODEL_MENU_LISTBOX_VERTICAL_PADDING =
32-
MODEL_MENU_LISTBOX_PADDING_TOP + MODEL_MENU_LISTBOX_PADDING_BOTTOM;
31+
const MODEL_MENU_LISTBOX_VERTICAL_PADDING = MODEL_MENU_LISTBOX_PADDING_BOTTOM;
3332
const MODEL_MENU_OVERSCAN_ROWS = 16;
3433
const MODEL_DESCRIPTION_TOOLTIP_DELAY_MS = 1000;
3534

@@ -54,6 +53,7 @@ export interface ProviderModelMenuProps {
5453
currentModel: string;
5554
/** When set, only this provider's rows are rendered. */
5655
lockedAgentKind?: string;
56+
presentationMode?: ThreadPresentationMode;
5757
isDisabled?: boolean;
5858
hideLabelOnWrap?: boolean;
5959
forceHideLabel?: boolean;
@@ -195,12 +195,21 @@ function splitModelLabel(label: string): { name: string; hint?: string } {
195195
};
196196
}
197197

198+
function refsForPresentation(
199+
refs: readonly ModelRef[],
200+
presentationMode: ThreadPresentationMode | undefined,
201+
): readonly ModelRef[] {
202+
if (!presentationMode) return refs;
203+
return refs.filter((ref) => ref.presentationMode === presentationMode);
204+
}
205+
198206
export function ProviderModelMenu(props: ProviderModelMenuProps) {
199207
const {
200208
providers,
201209
currentAgentKind,
202210
currentModel,
203211
lockedAgentKind,
212+
presentationMode,
204213
isDisabled,
205214
hideLabelOnWrap,
206215
forceHideLabel = false,
@@ -268,8 +277,15 @@ export function ProviderModelMenu(props: ProviderModelMenuProps) {
268277
// cheap as a trigger label lookup.
269278
const deferredAgentKind = useDeferredValue(currentAgentKind);
270279
const deferredModel = useDeferredValue(effectiveCurrentModel);
271-
const sectionFavorites = isOpen ? (sessionFavorites ?? favorites) : favorites;
272-
const sectionRecents = isOpen ? (sessionRecents ?? recents) : recents;
280+
const activeFavorites = refsForPresentation(favorites, presentationMode);
281+
const sectionFavorites = refsForPresentation(
282+
isOpen ? (sessionFavorites ?? favorites) : favorites,
283+
presentationMode,
284+
);
285+
const sectionRecents = refsForPresentation(
286+
isOpen ? (sessionRecents ?? recents) : recents,
287+
presentationMode,
288+
);
273289
const items = isOpen
274290
? buildProviderModelItems({
275291
providers,
@@ -278,7 +294,7 @@ export function ProviderModelMenu(props: ProviderModelMenuProps) {
278294
currentAgentKind: deferredAgentKind,
279295
currentModel: deferredModel,
280296
favorites: sectionFavorites,
281-
favoriteStateRefs: favorites,
297+
favoriteStateRefs: activeFavorites,
282298
recents: sectionRecents,
283299
})
284300
: [];
@@ -391,7 +407,9 @@ export function ProviderModelMenu(props: ProviderModelMenuProps) {
391407
items={items}
392408
selectedKeys={selectedKeys}
393409
scrollRef={windowedListRef}
394-
toggleFavorite={toggleFavoriteModel}
410+
toggleFavorite={(providerKind, modelId) =>
411+
toggleFavoriteModel(providerKind, modelId, presentationMode ?? "terminal")
412+
}
395413
onSelect={handleSelect}
396414
/>
397415
)}
@@ -553,7 +571,7 @@ function WindowedProviderModelList(props: {
553571
aria-activedescendant={
554572
activeIndex >= 0 ? `${domIdPrefix}-${items[activeIndex]?.id}` : undefined
555573
}
556-
className="lightcode-model-menu-listbox no-scrollbar max-h-72 overflow-y-auto pt-1.5 pb-1.5 outline-none"
574+
className="lightcode-model-menu-listbox no-scrollbar max-h-72 overflow-y-auto pb-1.5 outline-none"
557575
style={{ height: viewportHeight }}
558576
tabIndex={0}
559577
onScroll={(event) => {
@@ -766,8 +784,7 @@ function StickyWindowedHeader(props: {
766784
return (
767785
<div
768786
data-sticky-windowed-header=""
769-
className="sticky z-20 h-0 overflow-visible"
770-
style={{ top: MODEL_MENU_LISTBOX_PADDING_TOP }}
787+
className="sticky top-0 z-20 h-0 overflow-visible"
771788
aria-hidden="true"
772789
>
773790
{content}

src/renderer/components/common/ProviderModelMenu/parts/buildItems.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AgentCapability } from "@/shared/contracts";
1+
import type { AgentCapability, ThreadPresentationMode } from "@/shared/contracts";
22
import { deriveSubProvider, listSubProviderOrder } from "./deriveSubProvider";
33
import type { ProviderModelItem } from "./types";
44

@@ -12,6 +12,7 @@ export interface ProviderModelMenuProvider {
1212
export interface ModelRef {
1313
agentKind: string;
1414
modelId: string;
15+
presentationMode?: ThreadPresentationMode;
1516
}
1617

1718
export interface BuildProviderModelItemsInput {
Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
1-
import type { StatusTone } from "../statusTone";
2-
import { StatusIcon } from "../StatusIcon";
1+
import { createProviderIcon } from "../common/createProviderIcon";
32

43
const CLAUDE_PATH =
54
"m3.127 10.604 3.135-1.76.053-.153-.053-.085H6.11l-.525-.032-1.791-.048-1.554-.065-1.505-.08-.38-.081L0 7.832l.036-.234.32-.214.455.04 1.009.069 1.513.105 1.097.064 1.626.17h.259l.036-.105-.089-.065-.068-.064-1.566-1.062-1.695-1.121-.887-.646-.48-.327-.243-.306-.104-.67.435-.48.585.04.15.04.593.456 1.267.981 1.654 1.218.242.202.097-.068.012-.049-.109-.181-.9-1.626-.96-1.655-.428-.686-.113-.411a2 2 0 0 1-.068-.484l.496-.674L4.446 0l.662.089.279.242.411.94.666 1.48 1.033 2.014.302.597.162.553.06.17h.105v-.097l.085-1.134.157-1.392.154-1.792.052-.504.25-.605.497-.327.387.186.319.456-.045.294-.19 1.23-.37 1.93-.243 1.29h.142l.161-.16.654-.868 1.097-1.372.484-.545.565-.601.363-.287h.686l.505.751-.226.775-.707.895-.585.759-.839 1.13-.524.904.048.072.125-.012 1.897-.403 1.024-.186 1.223-.21.553.258.06.263-.218.536-1.307.323-1.533.307-2.284.54-.028.02.032.04 1.029.098.44.024h1.077l2.005.15.525.346.315.424-.053.323-.807.411-3.631-.863-.872-.218h-.12v.073l.726.71 1.331 1.202 1.667 1.55.084.383-.214.302-.226-.032-1.464-1.101-.565-.497-1.28-1.077h-.084v.113l.295.432 1.557 2.34.08.718-.112.234-.404.141-.444-.08-.911-1.28-.94-1.44-.759-1.291-.093.053-.448 4.821-.21.246-.484.186-.403-.307-.214-.496.214-.98.258-1.28.21-1.016.19-1.263.112-.42-.008-.028-.092.012-.953 1.307-1.448 1.957-1.146 1.227-.274.109-.477-.247.045-.44.266-.39 1.586-2.018.956-1.25.617-.723-.004-.105h-.036l-4.212 2.736-.75.096-.324-.302.04-.496.154-.162 1.267-.871z";
65

7-
export function ClaudeIcon(props: { tone?: StatusTone; className?: string }) {
8-
return (
9-
<StatusIcon
10-
cssPrefix="lightcode-claude-icon"
11-
path={CLAUDE_PATH}
12-
tone={props.tone ?? "inactive"}
13-
viewBox="0 0 16 16"
14-
className={props.className}
15-
/>
16-
);
17-
}
6+
export const ClaudeIcon = createProviderIcon({
7+
cssPrefix: "lightcode-claude-icon",
8+
path: CLAUDE_PATH,
9+
viewBox: "0 0 16 16",
10+
});

0 commit comments

Comments
 (0)