Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/daemon/src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Cause, Logger as EffectLogger } from 'effect';
import { castArray } from 'foxts/cast-array';
import type { ErrorLikeObject } from 'foxts/extract-error-message';
import { isErrorLikeObject } from 'foxts/extract-error-message';
import type { DestinationStream, Logger as PinoLogger } from 'pino';
Expand Down Expand Up @@ -99,7 +100,7 @@ function effectError(
export function createEffectLogger(target: PinoLogger): EffectLogger.Logger<unknown, void> {
return EffectLogger.make(({ cause, logLevel, message }) => {
if (logLevel === 'None') return;
const messages = Array.isArray(message) ? message : [message];
const messages = castArray(message);
const text = messages.find((entry) => typeof entry === 'string') ?? 'Effect log';
const bindings: Record<string, unknown> = effectBindings(messages);
const error = effectError(messages, cause);
Expand Down
2 changes: 1 addition & 1 deletion apps/daemon/tests/integration/pty-sidecar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const BINARY =
join(repoRoot, 'target', 'release', binaryName),
].find((path) => !!path && existsSync(path)) ?? '';

if (process.env.LINKCODE_REQUIRE_PTY_SIDECAR === '1' && !BINARY) {
if (!BINARY && process.env.LINKCODE_REQUIRE_PTY_SIDECAR === '1') {
throw new Error(
'LINKCODE_REQUIRE_PTY_SIDECAR=1 but linkcode-pty was not found; build it or set LINKCODE_PTY_SIDECAR_PATH',
);
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/scripts/verify-artifacts.mts
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,9 @@ function verifyNoAgentBinaries(resourceDir: string, asarPath: string, problems:
'app.asar.unpacked',
...prefix.split('/').slice(0, -1),
);
const base = prefix.split('/').at(-1)!;
if (!existsSync(dir)) continue;

const base = prefix.split('/').at(-1)!;
for (const entry of readdirSync(dir)) {
if (entry.startsWith(base)) {
problems.push(`${resourceDir}/app.asar.unpacked: agent platform package shipped: ${entry}`);
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/renderer/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
WorkbenchAppProviders,
WorkbenchProviders,
} from '@linkcode/workbench';
import { useAbortableEffect } from 'foxact/use-abortable-effect';
import { useEffect } from 'foxact/use-abortable-effect';
import { useState } from 'react';
import { DesktopAutomationsView } from './automations/automations-view';
import { desktopDaemonConnectionSource } from './daemon-connection-source';
Expand Down Expand Up @@ -93,7 +93,7 @@ function DesktopConnectionFallback(): React.ReactNode {
const [withinStartupGrace, setWithinStartupGrace] = useState(
() => Date.now() - RENDERER_BOOT_AT < MANAGED_STARTUP_GRACE_MS,
);
useAbortableEffect((signal) => {
useEffect((signal) => {
const remaining = MANAGED_STARTUP_GRACE_MS - (Date.now() - RENDERER_BOOT_AT);
if (remaining <= 0) return;
const timer = setTimeout(() => {
Expand All @@ -110,7 +110,7 @@ function DesktopConnectionFallback(): React.ReactNode {
function useDaemonIsManaged(): boolean {
const daemonUrlOverride = useDesktopSettingsStore((state) => state.daemonUrlOverride);
const [managed, setManaged] = useState(false);
useAbortableEffect(
useEffect(
(signal) => {
void systemBridge.daemon.isManaged().then((value) => {
if (!signal.aborted) setManaged(value);
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/src/settings/about-tab.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { UpdaterStatus } from '@linkcode/ipc';
import { Button } from 'coss-ui/components/button';
import { Field, FieldLabel } from 'coss-ui/components/field';
import { useEffect as useAbortableEffect } from 'foxact/use-abortable-effect';
import { useEffect } from 'foxact/use-abortable-effect';
import { useState } from 'react';
import { useTranslations } from 'use-intl';
import { systemBridge } from '../ipc';
Expand All @@ -20,7 +20,7 @@ export function AboutTab(): React.ReactNode {
const [version, setVersion] = useState('');
const [status, setStatus] = useState<UpdaterStatus>('idle');

useAbortableEffect((signal) => {
useEffect((signal) => {
void systemBridge.app.version().then((value) => {
if (!signal.aborted) setVersion(value);
});
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/src/settings/history-import-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from '@linkcode/workbench';
import { Button } from 'coss-ui/components/button';
import { noop } from 'foxact/noop';
import { useEffect as useAbortableEffect } from 'foxact/use-abortable-effect';
import { useEffect } from 'foxact/use-abortable-effect';
import { RotateCwIcon } from 'lucide-react';
import { useState } from 'react';
import { useTranslations } from 'use-intl';
Expand Down Expand Up @@ -45,7 +45,7 @@ export function HistoryImportTab({ kind }: { kind: AgentKind }): React.ReactNode
importableCount: bulk.importableCount,
});

useAbortableEffect(() => {
useEffect(() => {
if (onboardingAction !== 'complete') return;
void markOnboardingHandled().catch(noop);
}, [onboardingAction, markOnboardingHandled]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BrowserPane } from '@linkcode/ui/shell/browser';
import type { WebviewTag } from 'electron';
import { useEffect as useAbortableEffect } from 'foxact/use-abortable-effect';
import { useEffect } from 'foxact/use-abortable-effect';
import { noop } from 'foxts/noop';
import { useState } from 'react';
import { useTranslations } from 'use-intl';
Expand Down Expand Up @@ -47,7 +47,7 @@ export function BrowserWebviewPane(): React.ReactNode {
setWebview(element as WebviewTag | null);
};

useAbortableEffect(
useEffect(
(signal) => {
if (webview === null) return;
const sync = (): void => {
Expand Down Expand Up @@ -91,7 +91,7 @@ export function BrowserWebviewPane(): React.ReactNode {
// Pause any playing media when the pane is hidden (panel collapsed or another section shown),
// so a preview stops instead of playing audio out of sight. Paused, not resumed — the user
// restarts it on their next visit.
useAbortableEffect(() => {
useEffect(() => {
if (webview === null || visible) return;
// Guest may be mid-navigation or detached, in which case there is nothing to pause.
webview
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ShellIconButton } from '@linkcode/ui';
import { systemBridge } from '@renderer/ipc';
import { useEffect as useAbortableEffect } from 'foxact/use-abortable-effect';
import { useEffect } from 'foxact/use-abortable-effect';
import { CopyIcon, MinusIcon, SquareIcon, XIcon } from 'lucide-react';
import { useState } from 'react';
import type { DesktopChromeMetricsStyle } from './metrics';
Expand Down Expand Up @@ -35,7 +35,7 @@ export function DesktopWindowControls(): React.ReactNode {
*/
function WindowControls(): React.ReactNode {
const [maximized, setMaximized] = useState(false);
useAbortableEffect((signal) => {
useEffect((signal) => {
void systemBridge.window.isMaximized().then((value) => {
if (!signal.aborted) setMaximized(value);
});
Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/src/renderer/src/shell/desktop-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
useSelectedHostStore,
WorkspaceServicesMenu,
} from '@linkcode/workbench';
import { useEffect as useAbortableEffect } from 'foxact/use-abortable-effect';
import { useEffect } from 'foxact/use-abortable-effect';
import { useLayoutEffect } from 'foxact/use-isomorphic-layout-effect';
import { useSingleton } from 'foxact/use-singleton';
import { useCallback, useRef, useState } from 'react';
Expand Down Expand Up @@ -214,9 +214,9 @@ export function DesktopShell({
// Tab content mounts lazily on the panel's first settled open, so no shell is spawned for a
// panel that is never shown. Latched during render — React's prescribed state adjustment.
const [rightContentMounted, setRightContentMounted] = useState(false);
if (rightTransition.phase === 'open' && !rightContentMounted) setRightContentMounted(true);
if (!rightContentMounted && rightTransition.phase === 'open') setRightContentMounted(true);
const [bottomContentMounted, setBottomContentMounted] = useState(false);
if (bottomTransition.phase === 'open' && !bottomContentMounted) setBottomContentMounted(true);
if (!bottomContentMounted && bottomTransition.phase === 'open') setBottomContentMounted(true);

const hasNativeTrafficLights = desktopPlatform === 'darwin';
const hasNativeBackdrop = desktopPlatform === 'darwin' || desktopPlatform === 'win32';
Expand All @@ -226,7 +226,7 @@ export function DesktopShell({
const expandedPanel = getExpandedPanel(expansionStack, rightPanel.open, bottomPanel.open);
const chromeSurface = getChromeSurface(expandedPanel);

useAbortableEffect(
useEffect(
(signal) => {
void systemBridge.app.version().then((value) => {
if (!signal.aborted) setAppVersion(`v${value}`);
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/src/shell/layout/pane-transition.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useAbortableEffect } from 'foxact/use-abortable-effect';
import { useEffect } from 'foxact/use-abortable-effect';
import { useLayoutEffect } from 'foxact/use-isomorphic-layout-effect';
import { useReducedMotion } from 'motion/react';
import { useCallback, useReducer, useRef } from 'react';
Expand Down Expand Up @@ -95,7 +95,7 @@ export function usePaneTransition({
onSizeChange?.(open ? Math.max(0, size) : 0);
}, [onSizeChange, open, size]);

useAbortableEffect(() => {
useEffect(() => {
if (transitionVersion === 0) return;
if (!isAnimatingPhase(phase)) return;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { PanelSide } from '@linkcode/ui/shell/panels';
import type { PaletteCommand } from '@linkcode/workbench';
import { useCommandPaletteStore } from '@linkcode/workbench';
import { noop } from 'foxact/noop';
import { useAbortableEffect } from 'foxact/use-abortable-effect';
import { useEffect } from 'foxact/use-abortable-effect';
import { useTranslations } from 'use-intl';
import { openDesktopSettings } from '../settings/store';

Expand Down Expand Up @@ -33,7 +33,7 @@ export function useDesktopPaletteCommands({
}: UseDesktopPaletteCommandsOptions): void {
const tPalette = useTranslations('workbench.palette');

useAbortableEffect(() => {
useEffect(() => {
const settingsShortcut = formatKeyboardShortcut(
SETTINGS_SHORTCUT,
desktopPlatform === 'darwin' ? 'mac' : 'non-mac',
Expand Down
6 changes: 3 additions & 3 deletions apps/mobile/src/app/host/[hostId]/terminal/[terminalId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { useLinkCodeClient } from '@linkcode/client-core';
import type { TerminalMetadata, TerminalReplayEvent } from '@linkcode/schema';
import { TerminalIdSchema } from '@linkcode/schema';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { useEffect as useAbortableEffect } from 'foxact/use-abortable-effect';
import { useEffect } from 'foxact/use-abortable-effect';
import { extractErrorMessage } from 'foxts/extract-error-message';
import { Button, Chip, Spinner } from 'heroui-native';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import { Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTranslations } from 'use-intl';
Expand Down Expand Up @@ -41,7 +41,7 @@ export default function TerminalScreen(): React.ReactNode {
const [error, setError] = useState<string | null>(() => (terminalId ? null : t('invalidId')));
const [exit, setExit] = useState<{ code: number | null } | null>(null);

useAbortableEffect(
useEffect(
(signal) => {
if (!terminalId) return;
const offController = client.subscribeTerminalController(terminalId, (controlled) => {
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/src/app/host/[hostId]/terminal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useLinkCodeClient } from '@linkcode/client-core';
import type { TerminalMetadata } from '@linkcode/schema';
import { EmptyState, ScreenScroll } from '@linkcode/ui/native';
import { Stack, useLocalSearchParams, useRouter } from 'expo-router';
import { useEffect as useAbortableEffect } from 'foxact/use-abortable-effect';
import { useEffect } from 'foxact/use-abortable-effect';
import { extractErrorMessage } from 'foxts/extract-error-message';
import {
Button,
Expand Down Expand Up @@ -38,7 +38,7 @@ export default function TerminalsScreen(): React.ReactNode {

const load = useCallback(() => client.listTerminals(), [client]);

useAbortableEffect(
useEffect(
(signal) => {
void load()
.then((nextTerminals) => {
Expand Down
36 changes: 15 additions & 21 deletions apps/mobile/src/components/terminal-renderer.native.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { TerminalViewRef } from 'expo-libghostty';
import { TerminalView } from 'expo-libghostty';
import { useStableHandler } from 'foxact/use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired';
import { extractErrorMessage } from 'foxts/extract-error-message';
import { useEffect, useImperativeHandle, useRef } from 'react';
import { useCallback, useEffect, useEffectEvent, useImperativeHandle, useRef } from 'react';
import type { TerminalRendererProps } from './terminal-renderer.types';

/**
Expand All @@ -21,24 +22,18 @@ export default function TerminalRenderer({
onError,
}: TerminalRendererProps): React.ReactNode {
const terminalRef = useRef<TerminalViewRef>(null);
const canControlRef = useRef(canControl);
const readyRef = useRef(false);
const lastGridRef = useRef<{ cols: number; rows: number } | null>(null);
const onInputRef = useRef(onInput);
const onResizeRef = useRef(onResize);
const onReadyRef = useRef(onReady);
const onErrorRef = useRef(onError);

useEffect(() => {
onInputRef.current = onInput;
onResizeRef.current = onResize;
onReadyRef.current = onReady;
onErrorRef.current = onError;
}, [onError, onInput, onReady, onResize]);
const handleResize = useEffectEvent(onResize);
const handleError = useStableHandler(onError);

const reportError = (error: unknown) => {
onErrorRef.current(extractErrorMessage(error, false) ?? 'Unknown terminal renderer error');
};
const reportError = useCallback(
(error: unknown) => {
handleError(extractErrorMessage(error, false) ?? 'Unknown terminal renderer error');
},
[handleError],
);

useImperativeHandle(
ref,
Expand All @@ -56,14 +51,13 @@ export default function TerminalRenderer({
terminalRef.current?.finish(code ?? 0).catch(reportError);
},
}),
[],
[reportError],
);

useEffect(() => {
canControlRef.current = canControl;
// On gaining control, claim the PTY size for the local grid.
const grid = lastGridRef.current;
if (canControl && grid) onResizeRef.current(grid.cols, grid.rows);
if (canControl && grid) handleResize(grid.cols, grid.rows);
}, [canControl]);

return (
Expand All @@ -73,17 +67,17 @@ export default function TerminalRenderer({
fontSize={fontSize}
theme={theme}
onInput={({ nativeEvent }) => {
if (canControlRef.current) onInputRef.current(nativeEvent.text);
if (canControl) onInput(nativeEvent.text);
}}
onResize={({ nativeEvent }) => {
lastGridRef.current = { cols: nativeEvent.cols, rows: nativeEvent.rows };
// The first resize marks the ghostty surface as built — writes made
// before that would be dropped, so replay is gated on readiness.
if (!readyRef.current) {
readyRef.current = true;
onReadyRef.current();
onReady();
}
if (canControlRef.current) onResizeRef.current(nativeEvent.cols, nativeEvent.rows);
if (canControl) onResize(nativeEvent.cols, nativeEvent.rows);
}}
/>
);
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/src/runtime/use-seeded-conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Conversation, ConversationSeed, ConversationSeedEvent } from '@lin
import { useConversation, useLinkCodeClient } from '@linkcode/client-core';
import type { SessionId, SessionInfo } from '@linkcode/schema';
import { noop } from 'foxact/noop';
import { useEffect as useAbortableEffect } from 'foxact/use-abortable-effect';
import { useEffect } from 'foxact/use-abortable-effect';
import { useState } from 'react';

/** Upper bound on cursor pages one seed read follows, so a buggy cursor can't loop forever. */
Expand All @@ -22,7 +22,7 @@ export function useSeededConversation(session: SessionInfo | null): Conversation
const historyId = session?.historyId;
const sessionId = session?.sessionId ?? null;

useAbortableEffect(
useEffect(
(signal) => {
if (!agentKind || !historyId || !sessionId) return;
void (async () => {
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/src/runtime/use-workspaces.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useLinkCodeClient } from '@linkcode/client-core';
import type { WorkspaceRecord } from '@linkcode/schema';
import { noop } from 'foxact/noop';
import { useEffect as useAbortableEffect } from 'foxact/use-abortable-effect';
import { useEffect } from 'foxact/use-abortable-effect';
import { useCallback, useState } from 'react';

/** The host's registered workspaces (projects). A failed initial load degrades to the
Expand All @@ -14,7 +14,7 @@ export function useWorkspaces(): {
const [workspaces, setWorkspaces] = useState<WorkspaceRecord[]>([]);
const load = useCallback(() => client.listWorkspaces(), [client]);

useAbortableEffect(
useEffect(
(signal) => {
void load()
.then((next) => {
Expand Down
2 changes: 1 addition & 1 deletion apps/webview/e2e/browser-smoke.e2e.mts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function monitorApplicationErrors(page: Page, appOrigin: string, appErrors: stri
// Chromium reports route-change cancellations as ERR_ABORTED while the Vite module graph is
// still loading; a required canceled module is covered by the subsequent UI assertion.
const errorText = request.failure()?.errorText ?? 'unknown';
if (request.url().startsWith(appOrigin) && errorText !== 'net::ERR_ABORTED') {
if (errorText !== 'net::ERR_ABORTED' && request.url().startsWith(appOrigin)) {
appErrors.push(`requestfailed: ${request.method()} ${request.url()} (${errorText})`);
}
});
Expand Down
12 changes: 12 additions & 0 deletions eslint-suppressions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"apps/mobile/src/app/host/[hostId]/_layout.tsx": {
"sukka/react-no-use-effect-watching": {
"count": 1
}
},
"packages/presentation/ui/src/shell/composer-editor/editor.tsx": {
"sukka/react-no-use-effect-watching": {
"count": 2
}
}
}
2 changes: 1 addition & 1 deletion eslint.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ module.exports = require('eslint-config-sukka').sukka(
'packages/client/workbench/src/**',
'packages/client/core/src/**',
],
additionalHooks: '(useIsomorphicLayoutEffect|useAbortableEffect)',
additionalHooks: '(useIsomorphicLayoutEffect|useEffect)',
},
stylistic: false,
ts: {
Expand Down
Loading