Files
kandev/apps/web/lib/ssr/user-settings.ts
T
José Almeida 06e5114174 feat: add clickable terminal links with configurable open behavior (#401)
* feat: add clickable terminal links with configurable open behavior

Add @xterm/addon-web-links to both passthrough and shell terminals,
enabling Cmd+Click on URLs. Users can configure whether links open
in a new browser tab (default) or the built-in browser panel via a
new setting in the Settings page.

* style: fix prettier formatting

* fix: address CodeRabbit review comments

- Extract applyTerminalLinkBehavior helper to reduce complexity
- Set default terminalLinkBehavior for first-time users in empty settings path
- Use storeApi.getState() for optimistic rollback to avoid stale closures
- Add beforeEach settings reset in E2E tests
- Use 4xx range assertion instead of hardcoded 500

* fix: return 400 for user settings validation errors

Add ErrValidation sentinel to user service and wrap validation errors
from apply* functions. Handler now returns 400 Bad Request instead of
500 Internal Server Error for invalid input. Update E2E test assertion
to enforce client error status (< 500).

* fix: remove cmd+click mention from terminal links description

* fix: mirror validation error handling in WS update path

* style: fix prettier formatting
2026-03-10 15:37:51 +00:00

73 lines
2.6 KiB
TypeScript

import type { SavedLayout, UserSettingsResponse } from "@/lib/types/http";
export type UserSettingsData = NonNullable<UserSettingsResponse["settings"]>;
export function parseTerminalLinkBehavior(value: string | undefined): "new_tab" | "browser_panel" {
return value === "browser_panel" ? "browser_panel" : "new_tab";
}
export function buildCoreFields(s: UserSettingsData) {
return {
workspaceId: s.workspace_id || null,
workflowId: s.workflow_filter_id || null,
kanbanViewMode: s.kanban_view_mode || null,
repositoryIds: s.repository_ids ?? [],
preferredShell: s.preferred_shell || null,
defaultEditorId: s.default_editor_id || null,
enablePreviewOnClick: s.enable_preview_on_click ?? false,
chatSubmitKey: s.chat_submit_key ?? "cmd_enter",
reviewAutoMarkOnScroll: s.review_auto_mark_on_scroll ?? true,
showReleaseNotification: s.show_release_notification ?? true,
releaseNotesLastSeenVersion: s.release_notes_last_seen_version || null,
savedLayouts: s.saved_layouts ?? [],
defaultUtilityAgentId: s.default_utility_agent_id || null,
keyboardShortcuts: s.keyboard_shortcuts ?? {},
terminalLinkBehavior: parseTerminalLinkBehavior(s.terminal_link_behavior),
};
}
export function buildLspFields(s: UserSettingsData | undefined) {
return {
lspAutoStartLanguages: s?.lsp_auto_start_languages ?? [],
lspAutoInstallLanguages: s?.lsp_auto_install_languages ?? [],
lspServerConfigs: s?.lsp_server_configs ?? {},
};
}
/**
* Maps a `fetchUserSettings()` API response into the shape expected by `AppState["userSettings"]`.
* Use in SSR pages to build `initialState.userSettings`.
*/
export function mapUserSettingsResponse(response: UserSettingsResponse | null) {
const s = response?.settings;
const shellOptions = response?.shell_options ?? [];
if (!s) {
return {
workspaceId: null,
workflowId: null,
kanbanViewMode: null,
repositoryIds: [] as string[],
preferredShell: null,
shellOptions,
defaultEditorId: null,
enablePreviewOnClick: false,
chatSubmitKey: "cmd_enter" as const,
reviewAutoMarkOnScroll: true,
showReleaseNotification: true,
releaseNotesLastSeenVersion: null,
savedLayouts: [] as SavedLayout[],
defaultUtilityAgentId: null,
keyboardShortcuts: {} as Record<string, { key: string; modifiers?: Record<string, boolean> }>,
terminalLinkBehavior: "new_tab" as const,
...buildLspFields(undefined),
loaded: false,
};
}
return {
...buildCoreFields(s),
shellOptions,
...buildLspFields(s),
loaded: true,
};
}