chore: remove dead code (59 unused files + ~125 unused exports) (#1835)

* chore: remove dead/unreferenced files across ui, vscode

Remove 59 unused source files (components, hooks, lib utils, stores,
barrels, and orphaned vscode github modules) that are not imported by
any entry-reachable code. Also drop a stale test mock for the removed
execCommands module.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor: remove unused exported symbols (types, functions, consts, hooks)

Remove exported symbols whose identifier is referenced nowhere in the
repository (verified via repo-wide search), across ui types/contracts,
lib utilities, sync layer, stores, and components. Also drop the few
imports/private helpers orphaned by these removals.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor: remove more unused exports (desktop, shortcuts, worktree, vscode)

Continue removing repo-wide unreferenced exported functions, consts and
types across lib/desktop, shortcuts, worktreeSessionCreator, sync, and
vscode gitService, with cascading orphaned helpers/imports cleaned up.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* chore: add dead-code cleanup tooling

* refactor: checkpoint dead-code cleanup

* refactor: remove dead-code suppressions

---------

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Serhii Dziupin
2026-06-26 19:27:53 +03:00
committed by GitHub
co-authored by Serhii Dziupin Bohdan Triapitsyn
parent 4a37b9a005
commit 00821700de
324 changed files with 444 additions and 14876 deletions
-4
View File
@@ -30,8 +30,4 @@ export function getAgentColor(agentName: string | undefined) {
const paletteIndex = 1 + (Math.abs(hash) % (AGENT_COLOR_PALETTE.length - 1));
return AGENT_COLOR_PALETTE[paletteIndex];
}
export function getAgentColorPalette() {
return AGENT_COLOR_PALETTE;
}
+25 -42
View File
@@ -1,9 +1,9 @@
import type { WorktreeMetadata } from '@/types/worktree';
import type { DraftStarterRef } from '@/lib/draftStarters';
export type RuntimePlatform = 'web' | 'desktop' | 'vscode';
type RuntimePlatform = 'web' | 'desktop' | 'vscode';
export interface RuntimeDescriptor {
interface RuntimeDescriptor {
platform: RuntimePlatform;
isDesktop: boolean;
@@ -13,24 +13,18 @@ export interface RuntimeDescriptor {
label?: string;
}
export interface ApiError {
message: string;
code?: string;
cause?: unknown;
}
export interface Subscription {
interface Subscription {
close: () => void;
}
export interface RetryPolicy {
interface RetryPolicy {
maxRetries: number;
initialDelayMs: number;
maxDelayMs: number;
}
export interface TerminalTransportCapability {
interface TerminalTransportCapability {
preferred?: 'ws' | 'http' | 'sse';
transports?: Array<'ws' | 'http' | 'sse'>;
ws?: {
@@ -99,7 +93,7 @@ export interface TerminalAPI {
forceKill?(options: ForceKillOptions): Promise<void>;
}
export interface GitStatusFile {
interface GitStatusFile {
path: string;
index: string;
working_dir: string;
@@ -181,7 +175,7 @@ export interface GitBranch {
branches: Record<string, GitBranchDetails>;
}
export interface GitCommitSummary {
interface GitCommitSummary {
changes: number;
insertions: number;
deletions: number;
@@ -241,29 +235,18 @@ export interface CheckoutCommitResponse {
success: boolean;
}
export interface CherryPickRequest {
hash: string;
}
export interface CherryPickResponse {
success: boolean;
conflict?: boolean;
conflictFiles?: string[];
}
export interface RevertCommitRequest {
hash: string;
}
export interface RevertCommitResponse {
success: boolean;
conflict?: boolean;
conflictFiles?: string[];
}
export interface ResetToCommitRequest {
hash: string;
mode: 'soft' | 'mixed' | 'hard';
force?: boolean;
}
export interface ResetToCommitResponse {
success: boolean;
}
@@ -456,7 +439,7 @@ export interface GeneratedPullRequestDescription {
body: string;
}
export interface GitWorktreeAPI {
interface GitWorktreeAPI {
list(directory: string): Promise<GitWorktreeInfo[]>;
validate?(directory: string, payload: CreateGitWorktreePayload): Promise<GitWorktreeValidationResult>;
bootstrapStatus?(directory: string): Promise<GitWorktreeBootstrapStatus>;
@@ -591,11 +574,11 @@ export interface CommandExecResult {
error?: string;
}
export interface ListDirectoryOptions {
interface ListDirectoryOptions {
respectGitignore?: boolean;
}
export interface FileReadOptions {
interface FileReadOptions {
allowOutsideWorkspace?: boolean;
outsideFileGrant?: string;
optional?: boolean;
@@ -707,7 +690,7 @@ export interface DirectoryPermissionRequest {
path: string;
}
export interface DirectoryPermissionResult {
interface DirectoryPermissionResult {
success: boolean;
path?: string;
error?: string;
@@ -740,7 +723,7 @@ export interface NotificationsAPI {
canNotify?: () => boolean | Promise<boolean>;
}
export interface DiagnosticsAPI {
interface DiagnosticsAPI {
downloadLogs(): Promise<{ fileName: string; content: string }>;
}
@@ -796,13 +779,13 @@ export type GitHubUserSummary = {
email?: string;
};
export type GitHubRepoRef = {
type GitHubRepoRef = {
owner: string;
repo: string;
url: string;
};
export type GitHubChecksSummary = {
type GitHubChecksSummary = {
state: 'success' | 'failure' | 'pending' | 'unknown';
total: number;
success: number;
@@ -865,7 +848,7 @@ export type GitHubPullRequest = {
mergeableState?: string | null;
};
export type GitHubPullRequestHeadRepo = {
type GitHubPullRequestHeadRepo = {
owner: string;
repo: string;
url: string;
@@ -883,7 +866,7 @@ export type GitHubPullRequestSummary = GitHubPullRequest & {
sourceRepo?: (GitHubRepoSelector & { source: string }) | null;
};
export type GitHubPullRequestFile = {
type GitHubPullRequestFile = {
filename: string;
status?: string;
additions?: number;
@@ -892,7 +875,7 @@ export type GitHubPullRequestFile = {
patch?: string;
};
export type GitHubPullRequestReviewComment = {
type GitHubPullRequestReviewComment = {
id: number;
url: string;
body: string;
@@ -977,7 +960,7 @@ export type GitHubPullRequestMergeResult = {
message?: string;
};
export type GitHubIssueLabel = {
type GitHubIssueLabel = {
name: string;
color?: string;
};
@@ -1052,7 +1035,7 @@ export type GitHubAuthStatus = {
} | null;
};
export type GitHubAuthAccount = {
type GitHubAuthAccount = {
id: string;
user: GitHubUserSummary;
scope?: string;
@@ -1156,9 +1139,9 @@ export type RuntimeAPISelector<TValue> = (apis: RuntimeAPIs) => TValue;
// ============== Skills Catalog Types ==============
export type SkillsCatalogSourceId = string;
type SkillsCatalogSourceId = string;
export type SkillsCatalogSourceType = 'github' | 'clawdhub';
type SkillsCatalogSourceType = 'github' | 'clawdhub';
export interface SkillsCatalogSource {
id: SkillsCatalogSourceId;
@@ -1169,13 +1152,13 @@ export interface SkillsCatalogSource {
sourceType?: SkillsCatalogSourceType;
}
export interface SkillsCatalogItemInstalledBadge {
interface SkillsCatalogItemInstalledBadge {
isInstalled: boolean;
scope?: 'user' | 'project';
source?: 'opencode' | 'agents' | 'claude';
}
export interface ClawdHubSkillMetadata {
interface ClawdHubSkillMetadata {
slug: string;
version: string;
displayName?: string;
@@ -1224,7 +1207,7 @@ export interface SkillsRepoScanRequest {
gitIdentityId?: string;
}
export type SkillsRepoScanError =
type SkillsRepoScanError =
| { kind: 'authRequired'; message: string; sshOnly: true; identities?: Array<{ id: string; name: string }> }
| { kind: 'invalidSource'; message: string }
| { kind: 'gitUnavailable'; message: string }
@@ -1237,7 +1220,7 @@ export interface SkillsRepoScanResponse {
error?: SkillsRepoScanError;
}
export interface SkillsInstallSelection {
interface SkillsInstallSelection {
skillDir: string;
/** ClawdHub-specific metadata for installation */
clawdhub?: {
@@ -42,19 +42,6 @@ const extractRawAppearance = (data: unknown): RawAppearancePayload | null => {
return payload;
};
export const saveAppearancePreferences = (preferences: AppearancePreferences): boolean => {
if (typeof window === 'undefined') {
return false;
}
try {
localStorage.setItem('appearance-preferences', JSON.stringify(preferences));
return true;
} catch {
return false;
}
};
export const applyAppearancePreferences = (preferences: AppearancePreferences): void => {
const store = useUIStore.getState();
-352
View File
@@ -1,352 +0,0 @@
export const defaultCodeDark = {
'code[class*="language-"]': {
color: '#cdccc3',
background: 'transparent',
fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace',
fontSize: '1em',
textAlign: 'left' as const,
whiteSpace: 'pre',
wordSpacing: 'normal',
wordBreak: 'normal' as const,
wordWrap: 'normal' as const,
lineHeight: '1.5',
MozTabSize: '4',
OTabSize: '4',
tabSize: '4',
WebkitHyphens: 'none' as const,
MozHyphens: 'none' as const,
msHyphens: 'none' as const,
hyphens: 'none' as const,
},
'pre[class*="language-"]': {
color: '#cdccc3',
background: 'transparent',
fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace',
fontSize: '1em',
textAlign: 'left' as const,
whiteSpace: 'pre',
wordSpacing: 'normal',
wordBreak: 'normal' as const,
wordWrap: 'normal' as const,
lineHeight: '1.5',
MozTabSize: '4',
OTabSize: '4',
tabSize: '4',
WebkitHyphens: 'none' as const,
MozHyphens: 'none' as const,
msHyphens: 'none' as const,
hyphens: 'none' as const,
padding: '1em',
margin: '0',
overflow: 'auto',
},
comment: {
color: '#6b6964',
fontStyle: 'italic',
},
prolog: {
color: '#6b6964',
},
doctype: {
color: '#6b6964',
},
cdata: {
color: '#6b6964',
},
punctuation: {
color: '#6b6963',
},
property: {
color: '#d29470',
},
tag: {
color: '#d8886d',
},
boolean: {
color: '#d39373',
},
number: {
color: '#d39373',
},
constant: {
color: '#6cacd6',
},
symbol: {
color: '#6cacd6',
},
deleted: {
color: '#d98678',
},
selector: {
color: '#81af6c',
},
'attr-name': {
color: '#c2974d',
},
string: {
color: '#81af6c',
},
char: {
color: '#81af6c',
},
builtin: {
color: '#5aa9d9',
},
inserted: {
color: '#81af6c',
},
operator: {
color: '#d29470',
},
entity: {
color: '#edb449',
cursor: 'help',
},
url: {
color: '#5aa9d9',
},
'.language-css .token.string': {
color: '#81af6c',
},
'.style .token.string': {
color: '#81af6c',
},
variable: {
color: '#d29470',
},
atrule: {
color: '#c2974d',
},
'attr-value': {
color: '#81af6c',
},
function: {
color: '#5aa9d9',
},
'class-name': {
color: '#c2974d',
},
keyword: {
color: '#d8886d',
},
regex: {
color: '#81af6c',
},
important: {
color: '#d8886d',
fontWeight: 'bold',
},
bold: {
fontWeight: 'bold',
},
italic: {
fontStyle: 'italic',
},
namespace: {
opacity: 0.7,
},
title: {
color: '#edb449',
fontWeight: 'bold',
},
'code-block': {
color: '#81af6c',
},
'code-snippet': {
color: '#81af6c',
},
list: {
color: '#d29470',
},
hr: {
color: '#6b6963',
},
table: {
color: '#5aa9d9',
},
blockquote: {
color: '#6b6964',
fontStyle: 'italic',
},
strike: {
textDecoration: 'line-through',
},
};
export const defaultCodeLight = {
'code[class*="language-"]': {
color: '#403e3c',
background: 'transparent',
fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace',
fontSize: '1em',
textAlign: 'left' as const,
whiteSpace: 'pre',
wordSpacing: 'normal',
wordBreak: 'normal' as const,
wordWrap: 'normal' as const,
lineHeight: '1.5',
MozTabSize: '4',
OTabSize: '4',
tabSize: '4',
WebkitHyphens: 'none' as const,
MozHyphens: 'none' as const,
msHyphens: 'none' as const,
hyphens: 'none' as const,
},
'pre[class*="language-"]': {
color: '#403e3c',
background: 'transparent',
fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace',
fontSize: '1em',
textAlign: 'left' as const,
whiteSpace: 'pre',
wordSpacing: 'normal',
wordBreak: 'normal' as const,
wordWrap: 'normal' as const,
lineHeight: '1.5',
MozTabSize: '4',
OTabSize: '4',
tabSize: '4',
WebkitHyphens: 'none' as const,
MozHyphens: 'none' as const,
msHyphens: 'none' as const,
hyphens: 'none' as const,
padding: '1em',
margin: '0',
overflow: 'auto',
},
comment: {
color: '#7a756a',
fontStyle: 'italic',
},
prolog: {
color: '#7a756a',
},
doctype: {
color: '#7a756a',
},
cdata: {
color: '#7a756a',
},
punctuation: {
color: '#6b6963',
},
property: {
color: '#c07845',
},
tag: {
color: '#b96f55',
},
boolean: {
color: '#b97659',
},
number: {
color: '#b97659',
},
constant: {
color: '#4791ba',
},
symbol: {
color: '#4791ba',
},
deleted: {
color: '#c15748',
},
selector: {
color: '#6a9354',
},
'attr-name': {
color: '#9f7d3e',
},
string: {
color: '#6a9354',
},
char: {
color: '#6a9354',
},
builtin: {
color: '#4791ba',
},
inserted: {
color: '#6a9354',
},
operator: {
color: '#c07845',
},
entity: {
color: '#d09930',
cursor: 'help',
},
url: {
color: '#4791ba',
},
'.language-css .token.string': {
color: '#6a9354',
},
'.style .token.string': {
color: '#6a9354',
},
variable: {
color: '#c07845',
},
atrule: {
color: '#9f7d3e',
},
'attr-value': {
color: '#6a9354',
},
function: {
color: '#4791ba',
},
'class-name': {
color: '#9f7d3e',
},
keyword: {
color: '#b96f55',
},
regex: {
color: '#6a9354',
},
important: {
color: '#b96f55',
fontWeight: 'bold',
},
bold: {
fontWeight: 'bold',
},
italic: {
fontStyle: 'italic',
},
namespace: {
opacity: 0.7,
},
title: {
color: '#d09930',
fontWeight: 'bold',
},
'code-block': {
color: '#6a9354',
},
'code-snippet': {
color: '#6a9354',
},
list: {
color: '#c07845',
},
hr: {
color: '#6b6963',
},
table: {
color: '#4791ba',
},
blockquote: {
color: '#7a756a',
fontStyle: 'italic',
},
strike: {
textDecoration: 'line-through',
},
};
+4 -96
View File
@@ -5,9 +5,10 @@ import type { MobileKeyboardMode } from '@/lib/mobileKeyboardMode';
import { getRuntimeApiBaseUrl, getRuntimeKey } from '@/lib/runtime-switch';
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
export type AssistantNotificationPayload = {
title?: string;
body?: string;
type ManagedRemoteTunnelPreset = {
id: string;
name: string;
hostname: string;
};
export type UpdateInfo = {
@@ -35,12 +36,6 @@ export type SkillCatalogConfig = {
gitIdentityId?: string;
};
export type ManagedRemoteTunnelPreset = {
id: string;
name: string;
hostname: string;
};
export type DesktopSettings = {
themeId?: string;
useSystemTheme?: boolean;
@@ -526,28 +521,6 @@ export const stopAccessingDirectory = async (
return { success: true };
};
export const sendAssistantCompletionNotification = async (
payload?: AssistantNotificationPayload
): Promise<boolean> => {
if (hasDesktopInvoke()) {
try {
await invokeDesktop('desktop_notify', {
payload: {
title: payload?.title,
body: payload?.body,
tag: 'openchamber-agent-complete',
},
});
return true;
} catch (error) {
console.warn('Failed to send assistant completion notification', error);
return false;
}
}
return false;
};
export const checkForDesktopUpdates = async (): Promise<UpdateInfo | null> => {
if (!hasDesktopInvoke()) {
return null;
@@ -787,58 +760,6 @@ export const openDesktopFileInApp = async (
}
};
export const filterInstalledDesktopApps = async (apps: string[]): Promise<string[]> => {
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
return [];
}
const candidate = Array.isArray(apps) ? apps.filter((value) => typeof value === 'string') : [];
if (candidate.length === 0) {
return [];
}
try {
const result = await invokeDesktop<string[]>('desktop_filter_installed_apps', {
apps: candidate,
});
return Array.isArray(result) ? result.filter((value) => typeof value === 'string') : [];
} catch (error) {
console.warn('Failed to check installed apps', error);
return [];
}
};
export const fetchDesktopAppIcons = async (apps: string[]): Promise<Record<string, string>> => {
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
return {};
}
const candidate = Array.isArray(apps) ? apps.filter((value) => typeof value === 'string') : [];
if (candidate.length === 0) {
return {};
}
try {
const result = await invokeDesktop<unknown[]>('desktop_fetch_app_icons', {
apps: candidate,
});
if (!Array.isArray(result)) {
return {};
}
const map: Record<string, string> = {};
for (const entry of result) {
if (!entry || typeof entry !== 'object') continue;
const candidateEntry = entry as { app?: unknown; data_url?: unknown };
if (typeof candidateEntry.app !== 'string' || typeof candidateEntry.data_url !== 'string') continue;
map[candidateEntry.app] = candidateEntry.data_url;
}
return map;
} catch (error) {
console.warn('Failed to fetch installed app icons', error);
return {};
}
};
export type InstalledDesktopAppInfo = {
name: string;
iconDataUrl?: string | null;
@@ -898,16 +819,3 @@ export const fetchDesktopInstalledApps = async (
}
};
export const clearDesktopCache = async (): Promise<boolean> => {
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
return false;
}
try {
await invokeDesktop('desktop_clear_cache');
return true;
} catch (error) {
console.warn('Failed to clear cache', error);
return false;
}
};
-43
View File
@@ -24,27 +24,6 @@ export const startDesktopWindowDrag = async (): Promise<void> => {
}
};
export const isDesktopWindowFullscreen = async (): Promise<boolean> => {
if (!isDesktopShell()) {
return false;
}
try {
return Boolean(await invokeDesktopCommand('desktop_is_window_fullscreen'));
} catch {
return false;
}
};
export const onDesktopWindowResized = (handler: () => void): (() => void) => {
if (typeof window === 'undefined') {
return () => {};
}
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
};
export const setDesktopWindowTitle = async (title: string): Promise<void> => {
if (!isDesktopShell()) {
return;
@@ -84,25 +63,3 @@ export const getDesktopAppVersion = async (): Promise<string | null> => {
return null;
}
};
export const readDesktopFile = async (
path: string,
): Promise<{ mime: string; base64: string; size?: number }> => {
return invokeDesktopCommand('desktop_read_file', { path });
};
export const readDesktopFileAsDataUrl = async (path: string): Promise<string> => {
const result = await readDesktopFile(path);
return `data:${result.mime || 'application/octet-stream'};base64,${result.base64}`;
};
export const listenDesktopNativeDragDrop = async (
handler: (event: unknown) => void,
): Promise<(() => void) | null> => {
if (!isDesktopShell() || typeof window === 'undefined') {
return null;
}
void handler;
return null;
};
+5 -5
View File
@@ -9,11 +9,11 @@ type DesktopBridgeGlobal = {
) => Promise<() => void>;
};
export type DesktopSshRemoteMode = 'managed' | 'external';
export type DesktopSshInstallMethod = 'npm' | 'bun' | 'download_release' | 'upload_bundle';
export type DesktopSshSecretStore = 'never' | 'settings';
type DesktopSshRemoteMode = 'managed' | 'external';
type DesktopSshInstallMethod = 'npm' | 'bun' | 'download_release' | 'upload_bundle';
type DesktopSshSecretStore = 'never' | 'settings';
export type DesktopSshStoredSecret = {
type DesktopSshStoredSecret = {
enabled: boolean;
value?: string;
store: DesktopSshSecretStore;
@@ -62,7 +62,7 @@ export type DesktopSshInstancesConfig = {
instances: DesktopSshInstance[];
};
export type DesktopSshPhase =
type DesktopSshPhase =
| 'idle'
| 'config_resolved'
| 'auth_check'
+4 -10
View File
@@ -1,7 +1,7 @@
import React from 'react';
import { isDesktopShell, isVSCodeRuntime } from '@/lib/desktop';
export type DeviceType = 'desktop' | 'mobile' | 'tablet';
type DeviceType = 'desktop' | 'mobile' | 'tablet';
export interface DeviceInfo {
isMobile: boolean;
@@ -14,13 +14,7 @@ export interface DeviceInfo {
hasTouchOnlyPointer: boolean;
}
export const CSS_DEVICE_VARIABLES = {
IS_MOBILE: 'var(--is-mobile)',
DEVICE_TYPE: 'var(--device-type)',
HAS_TOUCH_INPUT: 'var(--has-touch-input)',
} as const;
export const BREAKPOINTS = {
const BREAKPOINTS = {
xs: 0,
sm: 640,
md: 768,
@@ -291,7 +285,7 @@ export function isMobileDeviceViaCSS(): boolean {
return isMobileValue === '1' || isMobileValue === 'true';
}
export const isStandalonePwaRuntime = (): boolean => {
const isStandalonePwaRuntime = (): boolean => {
if (typeof window === 'undefined') return false;
const standaloneNavigator = navigator as Navigator & { standalone?: boolean };
@@ -302,7 +296,7 @@ export const isStandalonePwaRuntime = (): boolean => {
);
};
export const isTabletStandalonePwaRuntime = (): boolean => {
const isTabletStandalonePwaRuntime = (): boolean => {
if (typeof window === 'undefined' || isDesktopShell()) return false;
const maxTouchPoints = typeof navigator !== 'undefined' ? navigator.maxTouchPoints ?? 0 : 0;
-17
View File
@@ -10,23 +10,6 @@ const PATCH_DIFF_CACHE_LIMIT = 64;
const DEFAULT_PATCH_CONTEXT_LINES = 3;
const patchFileDiffCache = new Map<string, FileDiffMetadata>();
export const fileDiffFromContent = (
file: string,
before: string,
after: string,
contextLines = DEFAULT_PATCH_CONTEXT_LINES
): FileDiffMetadata => {
if (!before && !after) {
return emptyFileDiff(file);
}
return parseDiffFromFile(
{ name: file, contents: before },
{ name: file, contents: after },
{ context: contextLines },
);
};
export const fileDiffFromPatch = (
file: string,
patch: string,
+1 -3
View File
@@ -20,7 +20,7 @@ const readStoredShowHidden = (): boolean => {
}
};
export const notifyDirectoryShowHiddenChanged = () => {
const notifyDirectoryShowHiddenChanged = () => {
if (typeof window === 'undefined') {
return;
}
@@ -67,5 +67,3 @@ export const useDirectoryShowHidden = (): boolean => {
return showHidden;
};
export const DIRECTORY_SHOW_HIDDEN_STORAGE_KEY = SHOW_HIDDEN_STORAGE_KEY;
-2
View File
@@ -34,8 +34,6 @@ export const BUILTIN_STARTERS: readonly BuiltInStarter[] = [
const BUILTIN_BY_NAME = new Map<string, BuiltInStarter>(BUILTIN_STARTERS.map((s) => [s.name, s]));
export const getBuiltInStarter = (name: string): BuiltInStarter | undefined => BUILTIN_BY_NAME.get(name);
export const isBuiltInStarter = (ref: DraftStarterRef): boolean =>
ref.type === 'command' && BUILTIN_BY_NAME.has(ref.name);
// Default global starter set (used until the user customizes the global list).
export const DEFAULT_GLOBAL_STARTERS: readonly DraftStarterRef[] = BUILTIN_STARTERS.map((s) => ({
-58
View File
@@ -1,58 +0,0 @@
import type { CommandExecResult, FilesAPI } from '@/lib/api/types';
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
import { runtimeFetch } from '@/lib/runtime-fetch';
type ExecResult = { success: boolean; results: CommandExecResult[] };
const DEFAULT_BASE_URL = import.meta.env.VITE_OPENCODE_URL || '/api';
const getBaseUrl = (): string => {
if (typeof DEFAULT_BASE_URL === 'string' && DEFAULT_BASE_URL.startsWith('/')) {
return DEFAULT_BASE_URL;
}
return DEFAULT_BASE_URL;
};
function getRuntimeFilesAPI(): FilesAPI | null {
const apis = getRegisteredRuntimeAPIs();
if (apis?.files) {
return apis.files;
}
return null;
}
export async function execCommands(commands: string[], cwd: string): Promise<ExecResult> {
const runtimeFiles = getRuntimeFilesAPI();
if (runtimeFiles?.execCommands) {
return runtimeFiles.execCommands(commands, cwd);
}
const response = await runtimeFetch(`${getBaseUrl()}/fs/exec`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ commands, cwd, background: false }),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: response.statusText }));
throw new Error((error as { error?: string }).error || 'Command exec failed');
}
const payload = (await response.json().catch(() => null)) as
| { success?: boolean; results?: CommandExecResult[] }
| null;
return {
success: Boolean(payload?.success),
results: Array.isArray(payload?.results) ? payload!.results! : [],
};
}
export async function execCommand(command: string, cwd: string): Promise<CommandExecResult> {
const result = await execCommands([command], cwd);
const first = result.results[0];
if (!first) {
return { command, success: result.success };
}
return first;
}
-2
View File
@@ -245,5 +245,3 @@ export const getFileTypeIconHref = (
const iconName = selectVariantIconName(baseIconName, options?.themeVariant || 'dark');
return `#${iconName}`;
};
export const getFileTypeIconUrl = getFileTypeIconHref;
@@ -18,7 +18,7 @@ const readStoredShowGitignored = (): boolean => {
}
};
export const notifyFilesViewShowGitignoredChanged = () => {
const notifyFilesViewShowGitignoredChanged = () => {
if (typeof window === 'undefined') {
return;
}
@@ -65,5 +65,3 @@ export const useFilesViewShowGitignored = (): boolean => {
return showGitignored;
};
export const FILES_VIEW_SHOW_GITIGNORED_STORAGE_KEY = SHOW_GITIGNORED_STORAGE_KEY;
@@ -3,8 +3,6 @@
* Uses Ubuntu-style adjective-noun word pairs for memorable, collision-resistant naming.
*/
import { getGitBranches } from '@/lib/gitApi';
const ADJECTIVES = [
'artful', 'bionic', 'cosmic', 'disco', 'focal', 'groovy', 'jammy', 'kinetic',
'lunar', 'noble', 'bold', 'brave', 'calm', 'eager', 'gentle', 'happy', 'keen',
@@ -41,36 +39,3 @@ export function generateBranchName(prefix?: string): string {
}
return slug;
}
/**
* Generate a unique branch name that doesn't conflict with existing branches.
* @param projectDirectory - Project directory to check for existing branches
* @param prefix - Optional branch prefix
* @param maxAttempts - Maximum attempts to generate a unique name (default: 10)
* @returns Unique branch name, or null if all attempts failed
*/
export async function generateUniqueBranchName(
projectDirectory: string,
prefix?: string,
maxAttempts: number = 10
): Promise<string | null> {
let existingBranches: Set<string>;
try {
const branches = await getGitBranches(projectDirectory);
existingBranches = new Set(branches?.all ?? []);
} catch {
// If we can't get branches, just generate without checking
return generateBranchName(prefix);
}
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const candidate = generateBranchName(prefix);
if (!existingBranches.has(candidate)) {
return candidate;
}
}
// All attempts exhausted, return null
return null;
}
+1 -1
View File
@@ -1,6 +1,6 @@
export { I18nProvider } from './context';
export { getBootstrapMessages, readStoredLocaleForBootstrap } from './bootstrap';
export { getCurrentIntlLocale, getIntlLocale } from './intl';
export { getCurrentIntlLocale } from './intl';
export { useI18n } from './useI18n';
export { formatMessage, initializeLocale, useI18nStore } from './store';
export type { I18nKey, I18nParams, Locale } from './store';
+1 -1
View File
@@ -13,6 +13,6 @@ const INTL_LOCALE_BY_LOCALE: Record<Locale, string> = {
pl: 'pl-PL',
};
export const getIntlLocale = (locale: Locale): string => INTL_LOCALE_BY_LOCALE[locale] ?? 'en-US';
const getIntlLocale = (locale: Locale): string => INTL_LOCALE_BY_LOCALE[locale] ?? 'en-US';
export const getCurrentIntlLocale = (): string => getIntlLocale(useI18nStore.getState().locale);
+1 -1
View File
@@ -61,7 +61,7 @@ export function normalizeLocale(value: string | undefined | null): Locale {
return DEFAULT_LOCALE;
}
export function readStoredLocale(): Locale | undefined {
function readStoredLocale(): Locale | undefined {
if (typeof window === 'undefined') {
return undefined;
}
+2 -46
View File
@@ -3,7 +3,7 @@
* Provides parsing, tree building, flattening, and path utilities.
*/
export type JsonTreeNodeType = 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null';
type JsonTreeNodeType = 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null';
export interface JsonTreeNode {
id: string;
@@ -44,7 +44,7 @@ function getType(value: unknown): JsonTreeNodeType {
return 'null';
}
export function getNodePath(pathSegments: string[]): string {
function getNodePath(pathSegments: string[]): string {
if (pathSegments.length === 0) return 'root';
let result = 'root';
for (const segment of pathSegments) {
@@ -57,38 +57,6 @@ export function getNodePath(pathSegments: string[]): string {
return result;
}
export function parseNodePath(pathKey: string): string[] {
if (pathKey === 'root' || pathKey === '') return [];
const withoutRoot = pathKey.startsWith('root.') ? pathKey.slice(5) : pathKey.startsWith('root[') ? pathKey.slice(4) : pathKey;
const segments: string[] = [];
let current = '';
let i = 0;
while (i < withoutRoot.length) {
const ch = withoutRoot[i];
if (ch === '.') {
if (current) segments.push(current);
current = '';
i++;
} else if (ch === '[') {
if (current) segments.push(current);
current = '';
i++;
let bracket = '';
while (i < withoutRoot.length && withoutRoot[i] !== ']') {
bracket += withoutRoot[i];
i++;
}
segments.push(bracket);
i++;
} else {
current += ch;
i++;
}
}
if (current) segments.push(current);
return segments;
}
let nodeCount = 0;
function buildTreeNode(
@@ -208,15 +176,3 @@ export function getExpandableIdsAboveDepth(root: JsonTreeNode | null, maxDepth:
return ids;
}
export function isJsonParseable(text: string): boolean {
if (!text || typeof text !== 'string') return false;
const trimmed = text.trim();
if (trimmed.length < 2) return false;
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return false;
try {
JSON.parse(trimmed);
return true;
} catch {
return false;
}
}
+2 -7
View File
@@ -64,7 +64,7 @@ export interface MagicPromptOverridesPayload {
const API_ENDPOINT = '/api/magic-prompts';
export const MAGIC_PROMPT_DEFINITIONS: readonly MagicPromptDefinition[] = [
const MAGIC_PROMPT_DEFINITIONS: readonly MagicPromptDefinition[] = [
{
id: 'git.commit.generate.visible',
title: 'Commit Generation Visible Prompt',
@@ -951,11 +951,6 @@ export const fetchMagicPromptOverrides = async (): Promise<Record<string, string
return inFlightOverridesRequest;
};
export const invalidateMagicPromptOverridesCache = () => {
cachedOverrides = null;
inFlightOverridesRequest = null;
};
export const getMagicPromptDefinition = (id: MagicPromptId): MagicPromptDefinition => {
const definition = MAGIC_PROMPT_DEFINITION_BY_ID.get(id);
if (!definition) {
@@ -968,7 +963,7 @@ export const getDefaultMagicPromptTemplate = (id: MagicPromptId): string => {
return getMagicPromptDefinition(id).template;
};
export const getEffectiveMagicPromptTemplate = async (id: MagicPromptId): Promise<string> => {
const getEffectiveMagicPromptTemplate = async (id: MagicPromptId): Promise<string> => {
const overrides = await fetchMagicPromptOverrides().catch((): Record<string, string> => ({}));
const override = overrides[id];
if (typeof override === 'string') {
+1 -91
View File
@@ -1,8 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Part } from "@opencode-ai/sdk/v2";
import { isFullySyntheticMessage } from "@/lib/messages/synthetic";
export interface MessageInfo {
interface MessageInfo {
id: string;
role: string;
time?: {
@@ -18,92 +17,3 @@ export interface MessageRecord {
info: MessageInfo & Record<string, any>;
parts: Part[];
}
export function isMessageComplete(messageInfo: MessageInfo, parts: Part[] = []): boolean {
if (isFullySyntheticMessage(parts)) {
return true;
}
const timeInfo = messageInfo?.time ?? {};
const completedAt = typeof timeInfo?.completed === 'number' ? timeInfo.completed : undefined;
const messageStatus = messageInfo?.status;
const hasStopFinish = messageInfo.finish === 'stop';
const hasCompletedFlag = (typeof completedAt === 'number' && completedAt > 0) || messageStatus === 'completed';
if (!hasCompletedFlag || !hasStopFinish) {
return false;
}
const hasActiveTools = parts.some((part) => {
switch (part.type) {
case 'reasoning': {
const time = (part as any)?.time;
return !time || typeof time.end === 'undefined';
}
case 'tool': {
const status = (part as any)?.state?.status;
return status === 'running' || status === 'pending';
}
default:
return false;
}
});
return !hasActiveTools;
}
export function getLatestAssistantMessageId(messages: MessageRecord[]): string | null {
const assistantMessages = messages
.filter(msg => msg.info.role === 'assistant' && !isFullySyntheticMessage(msg.parts))
.sort((a, b) => (a.info.id || "").localeCompare(b.info.id || ""));
return assistantMessages.length > 0
? assistantMessages[assistantMessages.length - 1].info.id
: null;
}
export function hasAnimatingWork(messages: MessageRecord[]): boolean {
if (messages.length === 0) {
return false;
}
for (const message of messages) {
if (message.info.role !== 'assistant') {
continue;
}
if (isFullySyntheticMessage(message.parts)) {
continue;
}
if (!isMessageComplete(message.info, message.parts)) {
return true;
}
}
return false;
}
export function shouldContinueStreaming(
messages: MessageRecord[],
currentStreamingId: string | null
): boolean {
const latestId = getLatestAssistantMessageId(messages);
if (!latestId) {
return false;
}
if (currentStreamingId && currentStreamingId !== latestId) {
return true;
}
const latestMessage = messages.find(
(msg) => msg.info.id === latestId && !isFullySyntheticMessage(msg.parts)
);
if (!latestMessage) {
return false;
}
return !isMessageComplete(latestMessage.info, latestMessage.parts);
}
@@ -1,176 +0,0 @@
const DB_NAME = 'openchamber-message-cursors';
const STORE_NAME = 'cursors';
const DB_VERSION = 1;
const FALLBACK_KEY = 'openchamber.messageCursors';
type CursorRecord = {
messageId: string;
completedAt: number;
};
const isBrowser = () => typeof window !== 'undefined';
const hasIndexedDbSupport = () => {
return isBrowser() && typeof indexedDB !== 'undefined';
};
const openDatabase = (): Promise<IDBDatabase> => {
if (!hasIndexedDbSupport()) {
return Promise.reject(new Error('IndexedDB not supported'));
}
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME);
}
};
request.onerror = () => {
reject(request.error ?? new Error('Failed to open IndexedDB'));
};
request.onsuccess = () => {
resolve(request.result);
};
});
};
let dbPromise: Promise<IDBDatabase> | null = null;
const getDatabase = (): Promise<IDBDatabase> => {
if (!dbPromise) {
dbPromise = openDatabase()
.then((db) => {
db.onclose = () => {
dbPromise = null;
};
db.onversionchange = () => {
db.close();
};
return db;
})
.catch((error: unknown) => {
dbPromise = null;
throw error;
});
}
return dbPromise;
};
const readFallback = (): Record<string, CursorRecord> => {
if (!isBrowser()) {
return {};
}
try {
const raw = window.localStorage.getItem(FALLBACK_KEY);
if (!raw) {
return {};
}
const parsed = JSON.parse(raw) as Record<string, CursorRecord>;
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
}
};
const writeFallback = (map: Record<string, CursorRecord>) => {
if (!isBrowser()) {
return;
}
try {
window.localStorage.setItem(FALLBACK_KEY, JSON.stringify(map));
} catch { /* ignored */ }
};
export const saveSessionCursor = async (
sessionId: string,
messageId: string,
completedAt: number
) => {
if (!sessionId || !messageId) {
return;
}
if (hasIndexedDbSupport()) {
try {
const db = await getDatabase();
await new Promise<void>((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readwrite');
const store = tx.objectStore(STORE_NAME);
store.put({ messageId, completedAt }, sessionId);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error ?? new Error('Cursor write failed'));
tx.onabort = () => reject(tx.error ?? new Error('Cursor write aborted'));
});
return;
} catch { /* ignored */ }
}
const fallback = readFallback();
fallback[sessionId] = { messageId, completedAt };
writeFallback(fallback);
};
export const readSessionCursor = async (
sessionId: string
): Promise<CursorRecord | null> => {
if (!sessionId) {
return null;
}
if (hasIndexedDbSupport()) {
try {
const db = await getDatabase();
const record = await new Promise<CursorRecord | null>((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readonly');
const store = tx.objectStore(STORE_NAME);
const request = store.get(sessionId);
request.onsuccess = () => {
resolve((request.result as CursorRecord) ?? null);
};
request.onerror = () => reject(request.error ?? new Error('Cursor read failed'));
tx.onerror = () => reject(tx.error ?? new Error('Cursor read failed'));
tx.onabort = () => reject(tx.error ?? new Error('Cursor read aborted'));
});
return record;
} catch { /* ignored */ }
}
const fallback = readFallback();
return fallback[sessionId] ?? null;
};
export const clearSessionCursor = async (sessionId: string) => {
if (!sessionId) {
return;
}
if (hasIndexedDbSupport()) {
try {
const db = await getDatabase();
await new Promise<void>((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readwrite');
const store = tx.objectStore(STORE_NAME);
store.delete(sessionId);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error ?? new Error('Cursor delete failed'));
tx.onabort = () => reject(tx.error ?? new Error('Cursor delete aborted'));
});
} catch { /* ignored */ }
}
const fallback = readFallback();
if (sessionId in fallback) {
delete fallback[sessionId];
writeFallback(fallback);
}
};
@@ -1,12 +1,12 @@
import type { Agent } from "@opencode-ai/sdk/v2";
export interface AgentMentionSource {
interface AgentMentionSource {
value: string;
start: number;
end: number;
}
export interface ParsedAgentMention {
interface ParsedAgentMention {
name: string;
source?: AgentMentionSource;
}
+1 -10
View File
@@ -1,9 +1,3 @@
export const EXECUTION_FORK_META_TEXT =
"This message comes from an AI assistant in another session. The user wants you to respond according to its content: " +
"if it is an implementation plan, your task is to implement that plan; " +
"if it is a conclusion or summary, your task is to verify it, explain whether you agree or disagree, and correct it if needed. " +
"Always clearly state what you understand your task to be, and wait for the user's approval of your conclusions before taking any further actions.";
export const MULTIRUN_EXECUTION_FORK_PROMPT_META_TEXT =
"This message bellow comes from an AI agent in another session. I want you to act according to its content: " +
"if it is an implementation plan, your task is to implement that plan; " +
@@ -12,9 +6,6 @@ export const MULTIRUN_EXECUTION_FORK_PROMPT_META_TEXT =
"Proceed with actions right away based on your understanding of the task. " +
"Here is the content of the message: ";
export const isExecutionForkMetaText = (text: string | null | undefined): boolean =>
typeof text === 'string' && text.trim() === EXECUTION_FORK_META_TEXT.trim();
// Default, user-editable instructions prefilled in the "Start new session from
// this answer" dialog. Mirrors the previous fixed fork instruction so existing
// behavior is preserved unless the user edits it.
@@ -26,7 +17,7 @@ export const EXECUTION_FORK_DEFAULT_INSTRUCTIONS =
// Fixed connective that opens the forked assistant content. Not editable by the
// user — it sits between the user's instructions and the assistant message.
export const EXECUTION_FORK_CONTENT_PREFACE =
const EXECUTION_FORK_CONTENT_PREFACE =
"This message below comes from an AI agent in another session. Here is the content of the message:";
// Builds the final message sent to the new session:
+2 -15
View File
@@ -4,7 +4,7 @@ import type { InlineCommentDraft } from '@/stores/useInlineCommentDraftStore';
* Format a single inline comment draft into the standard message format
* used by diff, plan, and file viewers
*/
export function formatInlineCommentDraft(draft: InlineCommentDraft): string {
function formatInlineCommentDraft(draft: InlineCommentDraft): string {
const { fileLabel, startLine, endLine, side, language, code, text } = draft;
// Diff format includes side (original/modified)
@@ -28,7 +28,7 @@ export function formatInlineCommentDraft(draft: InlineCommentDraft): string {
* Format multiple inline comment drafts into a single string
* with each comment separated by a blank line
*/
export function formatInlineCommentDrafts(drafts: InlineCommentDraft[]): string {
function formatInlineCommentDrafts(drafts: InlineCommentDraft[]): string {
if (drafts.length === 0) return '';
if (drafts.every((draft) => draft.source === 'preview-annotation')) {
@@ -55,16 +55,3 @@ export function appendInlineComments(text: string, drafts: InlineCommentDraft[])
return `${text}\n\n${formattedComments}`;
}
/**
* Check if a message text contains inline comments (for validation purposes)
*/
export function hasInlineComments(text: string): boolean {
return text.includes('Comment on `') && text.includes('```');
}
/**
* Extract the file label from a draft for display purposes
*/
export function getDraftDisplayLabel(draft: InlineCommentDraft): string {
return `${draft.fileLabel}:${draft.startLine}-${draft.endLine}`;
}
@@ -1,5 +1,5 @@
export const SKILL_LINK_PREFIX = '#openchamber-skill:';
export const AGENT_LINK_PREFIX = '#openchamber-agent:';
const SKILL_LINK_PREFIX = '#openchamber-skill:';
const AGENT_LINK_PREFIX = '#openchamber-agent:';
export const buildAgentMentionUrl = (name: string): string => {
const encoded = encodeURIComponent(name);
+4 -4
View File
@@ -1,8 +1,8 @@
export type MobileKeyboardMode = 'native' | 'resize-content';
export const MOBILE_KEYBOARD_MODE_STORAGE_KEY = 'openchamber.mobileKeyboardMode';
export const VIEWPORT_META_SELECTOR = 'meta[name="viewport"]';
export const VIEWPORT_CONTENT_BASE = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover';
const MOBILE_KEYBOARD_MODE_STORAGE_KEY = 'openchamber.mobileKeyboardMode';
const VIEWPORT_META_SELECTOR = 'meta[name="viewport"]';
const VIEWPORT_CONTENT_BASE = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover';
export const supportsMobileKeyboardResizeContent = (): boolean => {
return true;
@@ -21,7 +21,7 @@ export function normalizeMobileKeyboardMode(
return fallback;
}
export const getViewportContentForMobileKeyboardMode = (value: unknown): string => {
const getViewportContentForMobileKeyboardMode = (value: unknown): string => {
const mode = normalizeMobileKeyboardMode(value);
return mode === 'resize-content'
? `${VIEWPORT_CONTENT_BASE}, interactive-widget=resizes-content`
@@ -2,7 +2,7 @@ export type MobileLayoutPreference = 'default' | 'new';
const MOBILE_LAYOUT_PREFERENCE_KEY = 'openchamber-mobile-layout';
export const normalizeMobileLayoutPreference = (value: unknown): MobileLayoutPreference => {
const normalizeMobileLayoutPreference = (value: unknown): MobileLayoutPreference => {
// 'new' is the default; only an explicit 'default' (the legacy/"Old" layout)
// opts out of it.
return value === 'default' ? 'default' : 'new';
-4
View File
@@ -77,10 +77,6 @@ export const getMultiRunSessionTitle = (parts: {
return segments.join('/');
};
export const isMultiRunSessionTitle = (title?: string | null): boolean => {
return parseMultiRunSessionTitle(title) !== null;
};
export const getFusionSessionTitle = (groupSlug: string, providerID: string, modelID: string, runGroup?: string): string => {
const segments = [groupSlug];
if (runGroup) segments.push(runGroup);
+1 -1
View File
@@ -148,7 +148,7 @@ const formatLaunchRuntime = (wrapperType: string, node: string, bun: string): st
return 'direct executable';
};
export const buildOpenCodeStatusReport = async (): Promise<string> => {
const buildOpenCodeStatusReport = async (): Promise<string> => {
const now = new Date();
const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '(unknown)';
const platform = typeof navigator !== 'undefined' ? navigator.userAgent : '(no navigator)';
-5
View File
@@ -32,7 +32,6 @@ export const OPEN_IN_APPS: OpenInApp[] = [
export const DEFAULT_OPEN_IN_APP_ID = 'finder';
export const OPEN_IN_ALWAYS_AVAILABLE_APP_IDS = new Set(['finder', 'terminal']);
export const OPEN_DIRECTORY_APP_IDS = new Set(['finder', 'terminal', 'iterm2', 'ghostty']);
export const getPlatformOpenInApp = (app: OpenInApp): OpenInApp => {
if (typeof window !== 'undefined' && window.__OPENCHAMBER_PLATFORM__ === 'win32') {
@@ -50,7 +49,3 @@ export const getOpenInAppById = (id: string | null | undefined): OpenInApp | nul
const app = OPEN_IN_APPS.find((candidate) => candidate.id === id) ?? null;
return app ? getPlatformOpenInApp(app) : null;
};
export const getDefaultOpenInApp = (): OpenInApp => {
return getOpenInAppById(DEFAULT_OPEN_IN_APP_ID) ?? getPlatformOpenInApp(OPEN_IN_APPS[0]);
};
+13 -13
View File
@@ -30,7 +30,7 @@ function getRuntimeFilesAPI(): FilesAPI | null {
return null;
}
export interface OpenChamberConfig {
interface OpenChamberConfig {
projectPath?: string;
'setup-worktree'?: string[];
'setup-worktree-wait'?: boolean;
@@ -42,7 +42,7 @@ export interface OpenChamberConfig {
draftStarters?: DraftStarterRef[];
}
export type OpenChamberProjectActionPlatform = 'macos' | 'linux' | 'windows';
type OpenChamberProjectActionPlatform = 'macos' | 'linux' | 'windows';
export interface OpenChamberProjectAction {
id: string;
@@ -91,11 +91,11 @@ export interface OpenChamberProjectContextData extends OpenChamberProjectNotesTo
export const OPENCHAMBER_PROJECT_NOTES_MAX_LENGTH = 3000;
export const OPENCHAMBER_PROJECT_TODO_TEXT_MAX_LENGTH = 120;
export const OPENCHAMBER_PROJECT_ACTION_NAME_MAX_LENGTH = 80;
export const OPENCHAMBER_PROJECT_ACTION_COMMAND_MAX_LENGTH = 4000;
export const OPENCHAMBER_PROJECT_ACTION_OPEN_URL_MAX_LENGTH = 2000;
export const OPENCHAMBER_PROJECT_ACTION_DESKTOP_FORWARD_MAX_LENGTH = 300;
export const OPENCHAMBER_PROJECT_PLAN_TITLE_MAX_LENGTH = 160;
const OPENCHAMBER_PROJECT_ACTION_NAME_MAX_LENGTH = 80;
const OPENCHAMBER_PROJECT_ACTION_COMMAND_MAX_LENGTH = 4000;
const OPENCHAMBER_PROJECT_ACTION_OPEN_URL_MAX_LENGTH = 2000;
const OPENCHAMBER_PROJECT_ACTION_DESKTOP_FORWARD_MAX_LENGTH = 300;
const OPENCHAMBER_PROJECT_PLAN_TITLE_MAX_LENGTH = 160;
const OPENCHAMBER_ACTION_PLATFORM_SET = new Set<OpenChamberProjectActionPlatform>(['macos', 'linux', 'windows']);
@@ -520,7 +520,7 @@ const getProjectPlansDirectory = async (project: ProjectRef): Promise<string | n
return joinPath(projectDirectory, 'plans');
};
export const formatProjectPlanMarkdown = (title: string, body: string): string => {
const formatProjectPlanMarkdown = (title: string, body: string): string => {
const normalizedTitle = sanitizePlanTitle(title) || 'Plan';
const normalizedBody = body.trim();
return normalizedBody
@@ -552,7 +552,7 @@ export const parseProjectPlanMarkdown = (raw: string): { title: string; body: st
* Read the config for a project.
* Returns null if file doesn't exist or is invalid.
*/
export async function readOpenChamberConfig(project: ProjectRef): Promise<OpenChamberConfig | null> {
async function readOpenChamberConfig(project: ProjectRef): Promise<OpenChamberConfig | null> {
const projectDirectory = typeof project?.path === 'string' ? project.path.trim() : '';
if (!projectDirectory) {
return null;
@@ -624,7 +624,7 @@ export async function readOpenChamberConfig(project: ProjectRef): Promise<OpenCh
* dedicated route and never round-trips them through this config write path to
* avoid a read-then-write race clobbering a concurrent server update.
*/
export async function writeOpenChamberConfig(
async function writeOpenChamberConfig(
project: ProjectRef,
config: OpenChamberConfig
): Promise<boolean> {
@@ -678,7 +678,7 @@ export async function writeOpenChamberConfig(
/**
* Update specific keys in the config, preserving other values.
*/
export async function updateOpenChamberConfig(
async function updateOpenChamberConfig(
project: ProjectRef,
updates: Partial<OpenChamberConfig>
): Promise<boolean> {
@@ -753,12 +753,12 @@ export async function getProjectContextData(project: ProjectRef): Promise<OpenCh
});
}
export async function getProjectPlanFiles(project: ProjectRef): Promise<OpenChamberProjectPlanFileLink[]> {
async function getProjectPlanFiles(project: ProjectRef): Promise<OpenChamberProjectPlanFileLink[]> {
const config = await readOpenChamberConfig(project);
return sanitizeProjectPlanFileLinks(config?.projectPlanFiles);
}
export async function saveProjectPlanFiles(
async function saveProjectPlanFiles(
project: ProjectRef,
value: OpenChamberProjectPlanFileLink[]
): Promise<boolean> {
+1 -1
View File
@@ -1,7 +1,7 @@
import { getRuntimeUrlResolver } from './runtime-url';
import { subscribeRuntimeEndpointChanged } from './runtime-switch';
export type ScheduledTaskRanEvent = {
type ScheduledTaskRanEvent = {
type: 'scheduled-task-ran';
projectId: string;
taskId: string;
+2 -5
View File
@@ -7,7 +7,6 @@ import type {
Part,
Provider,
Config,
Model,
Agent,
TextPartInput,
FilePartInput,
@@ -170,7 +169,7 @@ interface App {
[key: string]: unknown;
}
export type FilesystemEntry = {
type FilesystemEntry = {
name: string;
path: string;
isDirectory: boolean;
@@ -203,7 +202,7 @@ type FileInputLite = {
url: string;
};
export type DirectorySwitchResult = {
type DirectorySwitchResult = {
success: boolean;
restarted: boolean;
path: string;
@@ -1830,5 +1829,3 @@ class OpencodeService {
export const opencodeClient = new OpencodeService();
// Exported types
export type { Session, Message, Part, Provider, Config, Model };
export type { App };
@@ -90,7 +90,7 @@ function isCircuitBreakerStatus(status?: number): boolean {
return status !== undefined && RETRYABLE_STATUS_CODES.has(status)
}
export function isCircuitOpen(providerID: string): boolean {
function isCircuitOpen(providerID: string): boolean {
const state = providers.get(providerID)
if (!state?.circuitOpen) return false
@@ -124,11 +124,3 @@ export function getRetryDelayMs(attempt: number): number {
const delay = DEFAULT_RETRY_BASE_DELAY_MS * 2 ** attempt
return Math.min(delay, DEFAULT_RETRY_MAX_DELAY_MS)
}
export function resetCircuit(providerID: string): void {
const state = providers.get(providerID)
if (!state) return
state.consecutiveErrors = 0
state.circuitOpen = false
state.circuitCooldownMs = DEFAULT_CIRCUIT_COOLDOWN_MS
}
+1 -1
View File
@@ -28,7 +28,7 @@ export const getOutsideFileGrant = (path: string): string | undefined => {
return entry.outsideFileGrant;
};
export const rememberOutsideFileGrant = (
const rememberOutsideFileGrant = (
path: string,
outsideFileGrant: string,
expiresAt?: number,
+1 -1
View File
@@ -54,7 +54,7 @@ const postJson = async (url: string, body?: unknown): Promise<Response> => runti
body: body === undefined ? undefined : JSON.stringify(body),
});
export const getPasskeyErrorMessage = async (response: Response, fallback: string): Promise<string> => {
const getPasskeyErrorMessage = async (response: Response, fallback: string): Promise<string> => {
try {
const payload = await response.json();
if (payload && typeof payload.error === 'string' && payload.error.trim()) {
+1 -1
View File
@@ -33,7 +33,7 @@ export const isAbsoluteFilePath = (value: string | null | undefined): boolean =>
return normalized.startsWith('/') || WINDOWS_DRIVE_ABSOLUTE_PATTERN.test(normalized);
};
export const toComparableFilePath = (value: string | null | undefined): string => {
const toComparableFilePath = (value: string | null | undefined): string => {
const normalized = normalizeFilePath(value);
return WINDOWS_DRIVE_ABSOLUTE_PATTERN.test(normalized) || normalized.startsWith('//')
? normalized.toLowerCase()
@@ -1,101 +0,0 @@
import type { EditPermissionMode } from '@/stores/types/sessionTypes';
export type BashPermissionValue = 'allow' | 'ask' | 'deny';
export type BashPermissionSetting = BashPermissionValue | Record<string, BashPermissionValue | undefined>;
export type SimplePermissionValue = 'allow' | 'ask' | 'deny' | undefined;
const isRecord = (value: unknown): value is Record<string, unknown> => {
return typeof value === 'object' && value !== null && !Array.isArray(value);
};
const isBashPermissionMap = (value: unknown): value is Record<string, BashPermissionValue | undefined> => {
return isRecord(value);
};
const hasBashAskEntry = (permission?: BashPermissionSetting): boolean => {
if (!permission) {
return false;
}
if (typeof permission === 'string') {
return permission === 'ask';
}
if (isBashPermissionMap(permission)) {
return Object.values(permission).some((value) => value === 'ask');
}
return false;
};
const hasBashDenyEntry = (permission?: BashPermissionSetting): boolean => {
if (!permission) {
return false;
}
if (typeof permission === 'string') {
return permission === 'deny';
}
if (isBashPermissionMap(permission)) {
return Object.values(permission).some((value) => value === 'deny');
}
return false;
};
export interface EditPermissionInputs {
agentDefaultEditMode: EditPermissionMode;
webfetchPermission?: SimplePermissionValue;
bashPermission?: BashPermissionSetting;
}
export interface EditPermissionUIState {
cascadeDefaultMode: EditPermissionMode;
modeAvailability: Record<EditPermissionMode, boolean>;
autoApproveAvailable: boolean;
bashHasAsk: boolean;
bashHasDeny: boolean;
bashAllAllow: boolean;
webfetchIsAllow: boolean;
webfetchNotDeny: boolean;
}
export const calculateEditPermissionUIState = ({
agentDefaultEditMode,
webfetchPermission,
bashPermission,
}: EditPermissionInputs): EditPermissionUIState => {
const bashHasAsk = hasBashAskEntry(bashPermission);
const bashHasDeny = hasBashDenyEntry(bashPermission);
const bashAllAllow = !bashHasAsk && !bashHasDeny;
const webfetchIsAllow = webfetchPermission === 'allow';
const webfetchNotDeny = webfetchPermission !== 'deny';
const editIsAllow = agentDefaultEditMode === 'allow' || agentDefaultEditMode === 'full';
const editIsAsk = agentDefaultEditMode === 'ask';
let cascadeDefaultMode: EditPermissionMode = agentDefaultEditMode;
if (editIsAllow && webfetchIsAllow && bashAllAllow) {
cascadeDefaultMode = 'full';
} else if (editIsAllow && bashHasAsk) {
cascadeDefaultMode = 'allow';
} else if (editIsAsk) {
cascadeDefaultMode = 'ask';
}
const modeAvailability: Record<EditPermissionMode, boolean> = {
ask: editIsAsk,
allow: editIsAsk || (editIsAllow && bashHasAsk),
full: agentDefaultEditMode !== 'deny' && webfetchNotDeny && bashHasAsk,
deny: false,
};
const autoApproveAvailable = modeAvailability.allow || modeAvailability.full;
return {
cascadeDefaultMode,
modeAvailability,
autoApproveAvailable,
bashHasAsk,
bashHasDeny,
bashAllAllow,
webfetchIsAllow,
webfetchNotDeny,
};
};
-28
View File
@@ -1,7 +1,3 @@
import type {
OpenChamberProjectAction,
OpenChamberProjectActionPlatform,
} from '@/lib/openchamberConfig';
import type {
DesktopSshInstance,
DesktopSshPortForward,
@@ -70,30 +66,6 @@ export const normalizeProjectActionDirectory = (value: string): string => {
return trimmed.length > 1 ? trimmed.replace(/\/+$/, '') : trimmed;
};
export const getCurrentProjectActionPlatform = (): OpenChamberProjectActionPlatform => {
if (typeof navigator === 'undefined') {
return 'macos';
}
const ua = (navigator.userAgent || '').toLowerCase();
if (ua.includes('windows')) {
return 'windows';
}
if (ua.includes('linux')) {
return 'linux';
}
return 'macos';
};
export const isProjectActionEnabledOnPlatform = (
action: OpenChamberProjectAction,
platform: OpenChamberProjectActionPlatform
): boolean => {
if (!Array.isArray(action.platforms) || action.platforms.length === 0) {
return true;
}
return action.platforms.includes(platform);
};
export const toProjectActionRunKey = (directory: string, actionId: string): string => {
return `${normalizeProjectActionDirectory(directory)}::${actionId}`;
};
+2 -2
View File
@@ -5,7 +5,7 @@ import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch';
import type { IconName } from "@/components/icon/icons";
type ThemeVariant = 'light' | 'dark';
export type ProjectIconImageOptions = { themeVariant?: ThemeVariant; iconColor?: string };
type ProjectIconImageOptions = { themeVariant?: ThemeVariant; iconColor?: string };
const PROJECT_ICON_OBJECT_URL_CACHE_LIMIT = 200;
@@ -150,7 +150,7 @@ const loadProjectIconObjectUrl = (
return promise;
};
export const useProjectIconImageObjectUrl = (
const useProjectIconImageObjectUrl = (
project: Pick<ProjectEntry, 'id' | 'iconImage'>,
options?: ProjectIconImageOptions,
): string | null => {
+1 -1
View File
@@ -26,7 +26,7 @@ export const resolveProjectForDirectory = (
return best;
};
export const resolveProjectFromWorktreeDirectory = (
const resolveProjectFromWorktreeDirectory = (
projects: ProjectEntry[],
availableWorktreesByProject: Map<string, WorktreeMetadata[]>,
directory: string | null,
-5
View File
@@ -8,7 +8,6 @@ export type PWADisplayMode =
const DISPLAY_MODES: Array<Exclude<PWADisplayMode, 'browser' | 'twa'>> = ['standalone', 'minimal-ui', 'fullscreen', 'window-controls-overlay'];
export const PWA_INSTALL_NAME_STORAGE_KEY = 'openchamber.pwaName';
export const PWA_RECENT_SESSIONS_STORAGE_KEY = 'openchamber.pwaRecentSessions';
const matchesDisplayMode = (mode: Exclude<PWADisplayMode, 'browser' | 'twa'>): boolean => {
@@ -35,7 +34,3 @@ export const getPWADisplayMode = (): PWADisplayMode => {
const matched = DISPLAY_MODES.find((mode) => matchesDisplayMode(mode));
return matched ?? 'browser';
};
export const isInstalledPWARuntime = (): boolean => {
return getPWADisplayMode() !== 'browser';
};
+2 -5
View File
@@ -1,16 +1,13 @@
export { QUOTA_PROVIDERS, QUOTA_PROVIDER_MAP } from './providers';
export type { QuotaProviderMeta } from './providers';
export { QUOTA_PROVIDERS } from './providers';
export {
clampPercent,
formatPercent,
formatQuotaValueLabel,
formatQuotaResetLabel,
resolveUsageTone,
formatWindowLabel,
calculatePace,
inferWindowSeconds,
getPaceStatusColor,
formatRemainingTime,
calculateExpectedUsagePercent,
} from './utils';
export type { PaceStatus, PaceInfo } from './utils';
export type { PaceInfo } from './utils';
+2 -17
View File
@@ -25,21 +25,6 @@ export function getDisplayModelName(modelName: string): string {
return modelName;
}
/**
* Get the auth source label from a model name prefix.
* e.g., "gemini/..." -> "Gemini"
* "antigravity/..." -> "Antigravity"
*/
export function getAuthSourceLabel(modelName: string): string | null {
const slashIndex = modelName.indexOf('/');
if (slashIndex === -1) return null;
const prefix = modelName.substring(0, slashIndex);
if (prefix === 'gemini') return 'Gemini';
if (prefix === 'antigravity') return 'Antigravity';
return null;
}
const GOOGLE_MODEL_FAMILIES: ModelFamily[] = [
{
id: 'gemini-auth',
@@ -55,11 +40,11 @@ const GOOGLE_MODEL_FAMILIES: ModelFamily[] = [
},
];
export const PROVIDER_MODEL_FAMILIES: Record<string, ModelFamily[]> = {
const PROVIDER_MODEL_FAMILIES: Record<string, ModelFamily[]> = {
google: GOOGLE_MODEL_FAMILIES,
};
export function getModelFamily(modelName: string, providerId: QuotaProviderId): ModelFamily | null {
function getModelFamily(modelName: string, providerId: QuotaProviderId): ModelFamily | null {
const families = PROVIDER_MODEL_FAMILIES[providerId] ?? [];
for (const family of families) {
if (family.matcher(modelName)) {
@@ -1,8 +0,0 @@
import type { ProviderResult, QuotaProviderId } from '@/types';
export interface QuotaProvider {
id: QuotaProviderId;
name: string;
isConfigured: () => Promise<boolean>;
fetchQuota: () => Promise<ProviderResult>;
}
@@ -21,10 +21,3 @@ export const QUOTA_PROVIDERS: QuotaProviderMeta[] = [
{ id: 'ollama-cloud', name: 'Ollama Cloud' },
{ id: 'wafer', name: 'Wafer.ai' },
];
export const QUOTA_PROVIDER_MAP = QUOTA_PROVIDERS.reduce<
Record<string, QuotaProviderMeta>
>((acc, provider) => {
acc[provider.id] = provider;
return acc;
}, {});
+1 -1
View File
@@ -132,7 +132,7 @@ export interface PaceInfo {
* Infer window duration in seconds from a window label.
* Used when the API doesn't provide windowSeconds directly.
*/
export const inferWindowSeconds = (label: string): number | null => {
const inferWindowSeconds = (label: string): number | null => {
const normalized = label.toLowerCase().trim();
// Exact matches
+1 -1
View File
@@ -26,7 +26,7 @@ export const getResponseStylePresetInstructions = (preset: ResponseStylePreset):
}
};
export const buildResponseStyleInstruction = ({
const buildResponseStyleInstruction = ({
enabled,
preset,
customInstructions,
-3
View File
@@ -5,7 +5,6 @@ import { flattenAssistantTextParts } from '@/lib/messages/messageText';
import {
getOriginalSessionID,
getReviewSessionID,
getSessionMetadata,
isReviewSession,
withoutReviewSessionLink,
withReviewSessionLink,
@@ -289,5 +288,3 @@ export const getReviewTransferDirection = (session: Session | null | undefined):
if (getReviewSessionID(session)) return 'original-to-review';
return null;
};
export const readSessionReviewMetadata = (session: Session | null | undefined) => getSessionMetadata(session);
+2 -5
View File
@@ -17,15 +17,12 @@
* - `/?tab=diff&file=src/main.ts` - Open diff view with file
*/
export type { RouteState, RouterContext } from './types';
export { VALID_TABS, VALID_SETTINGS_SECTIONS, ROUTE_PARAMS } from './types';
export type { RouteState } from './types';
export { parseRoute, hasRouteParams } from './parseRoute';
export type { AppRouteState } from './serializeRoute';
export {
serializeRoute,
buildURL,
routeMatchesURL,
updateBrowserURL,
} from './serializeRoute';
+3 -3
View File
@@ -21,7 +21,7 @@ const DEFAULT_TAB: MainTab = 'chat';
* Serialize application state to URL search parameters.
* Only includes parameters that differ from defaults to keep URLs clean.
*/
export function serializeRoute(state: AppRouteState): URLSearchParams {
function serializeRoute(state: AppRouteState): URLSearchParams {
const params = new URLSearchParams();
// Session ID - always include if present
@@ -54,7 +54,7 @@ export function serializeRoute(state: AppRouteState): URLSearchParams {
* Convert URLSearchParams to a URL string.
* Returns just the pathname if no params, otherwise pathname + search string.
*/
export function buildURL(params: URLSearchParams, pathname?: string): string {
function buildURL(params: URLSearchParams, pathname?: string): string {
const path = pathname ?? (typeof window !== 'undefined' ? window.location.pathname : '/');
const search = params.toString();
@@ -69,7 +69,7 @@ export function buildURL(params: URLSearchParams, pathname?: string): string {
* Check if the current URL matches the given route state.
* Used to avoid unnecessary URL updates.
*/
export function routeMatchesURL(state: AppRouteState): boolean {
function routeMatchesURL(state: AppRouteState): boolean {
if (typeof window === 'undefined') {
return true;
}
-10
View File
@@ -16,16 +16,6 @@ export interface RouteState {
diffFile: string | null;
}
/**
* Context for router operations - determines what capabilities are available.
*/
export interface RouterContext {
/** Whether running in VS Code webview (limited URL capabilities) */
isVSCode: boolean;
/** Whether URL can be updated (false in VS Code, true elsewhere) */
canUpdateURL: boolean;
}
/**
* Valid main tab values for URL routing.
*/
+2 -2
View File
@@ -1,4 +1,4 @@
export type RuntimeAuthCredential =
type RuntimeAuthCredential =
| { type: 'bearer'; token: string }
| null;
@@ -109,7 +109,7 @@ export const getRuntimeUrlAuthTokenSync = (): string => {
return token;
};
export const getRuntimeAuthCredential = async (): Promise<RuntimeAuthCredential> => {
const getRuntimeAuthCredential = async (): Promise<RuntimeAuthCredential> => {
const credential = await credentialProvider();
const token = credential?.type === 'bearer'
? normalizeBearerToken(credential.token)
-2
View File
@@ -150,5 +150,3 @@ export const configureRuntimeUrlResolver = (config: RuntimeUrlConfig): RuntimeUr
activeRuntimeUrlResolver = createRuntimeUrlResolver(config);
return activeRuntimeUrlResolver;
};
export const runtimeUrl = activeRuntimeUrlResolver;
+1 -1
View File
@@ -20,7 +20,7 @@ const isTouchOrCoarsePointer = (): boolean => {
return coarsePointer || touchPoints > 0;
};
export const detectHostedSurface = (): HostedSurface => {
const detectHostedSurface = (): HostedSurface => {
if (typeof window === 'undefined') return 'desktop';
const explicitSurface = window.__OPENCHAMBER_SURFACE__;
-27
View File
@@ -89,24 +89,6 @@ function getFuzzyMatchMask<T>(
return matches;
}
export function filterByFuzzyQuery<T>(
items: T[],
query: string,
getText: (item: T) => string,
options?: FuzzySearchOptions
): T[] {
const matches = getFuzzyMatchMask(items, query, getText, options);
const matching: T[] = [];
for (let i = 0; i < items.length; i++) {
if (matches[i]) {
matching.push(items[i]);
}
}
return matching;
}
/**
* Score-sorted fuzzy ranking. Strict (low threshold), prioritizes substring
* matches (especially prefix matches), and returns the top N.
@@ -159,15 +141,6 @@ export function scoreByFuzzyQuery<T>(
return scored.slice(0, limit);
}
export function rankByFuzzyQuery<T>(
items: T[],
query: string,
getText: (item: T) => string,
options?: { limit?: number; threshold?: number; noFuzzy?: boolean },
): T[] {
return scoreByFuzzyQuery(items, query, getText, options).map((x) => x.item);
}
export function partitionByFuzzyQuery<T>(
items: T[],
query: string,
@@ -1,53 +0,0 @@
import { describe, expect, test } from 'bun:test';
import { evaluateServerCompatibility, REQUIRED_SERVER_CAPABILITIES } from './server-compatibility';
const compatiblePayload = () => ({
status: 'ok',
openchamberVersion: '1.10.4',
runtime: 'web',
compatibility: {
apiVersion: 1,
minClientApiVersion: 1,
capabilities: [...REQUIRED_SERVER_CAPABILITIES],
},
});
describe('evaluateServerCompatibility', () => {
test('accepts a compatible server', () => {
const result = evaluateServerCompatibility(compatiblePayload());
expect(result.status).toBe('compatible');
expect(result.openchamberVersion).toBe('1.10.4');
expect(result.runtime).toBe('web');
});
test('rejects invalid compatibility payloads', () => {
expect(evaluateServerCompatibility({ status: 'ok' }).status).toBe('invalid-response');
expect(evaluateServerCompatibility(null).status).toBe('invalid-response');
});
test('detects old servers and old clients', () => {
expect(evaluateServerCompatibility({
...compatiblePayload(),
compatibility: { ...compatiblePayload().compatibility, apiVersion: 1 },
}, { clientApiVersion: 2 }).status).toBe('server-too-old');
expect(evaluateServerCompatibility({
...compatiblePayload(),
compatibility: { ...compatiblePayload().compatibility, minClientApiVersion: 2 },
}, { clientApiVersion: 1 }).status).toBe('client-too-old');
});
test('detects missing required capabilities', () => {
const result = evaluateServerCompatibility({
...compatiblePayload(),
compatibility: {
...compatiblePayload().compatibility,
capabilities: ['api.health.v1'],
},
});
expect(result.status).toBe('missing-capability');
expect(result.missingCapabilities).toContain('realtime.sse.v1');
});
});
-159
View File
@@ -1,159 +0,0 @@
import { runtimeFetch } from './runtime-fetch';
export const OPENCHAMBER_CLIENT_API_VERSION = 1;
export const REQUIRED_SERVER_CAPABILITIES = [
'api.health.v1',
'api.runtime-url.v1',
'api.raw-file.v1',
'realtime.sse.v1',
'realtime.websocket.global-events.v1',
] as const;
export type ServerCompatibilityStatus =
| 'compatible'
| 'auth-required'
| 'unreachable'
| 'invalid-response'
| 'server-too-old'
| 'client-too-old'
| 'missing-capability';
export interface ServerCompatibilityPayload {
status?: unknown;
openchamberVersion?: unknown;
runtime?: unknown;
compatibility?: {
apiVersion?: unknown;
minClientApiVersion?: unknown;
capabilities?: unknown;
} | null;
}
export interface ServerCompatibilityResult {
status: ServerCompatibilityStatus;
openchamberVersion: string | null;
runtime: string | null;
apiVersion: number | null;
minClientApiVersion: number | null;
missingCapabilities: string[];
requiredCapabilities: string[];
message: string;
}
const parsePositiveInteger = (value: unknown): number | null => {
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) return null;
return value;
};
const parseString = (value: unknown): string | null => {
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
};
const parseCapabilities = (value: unknown): Set<string> => {
if (!Array.isArray(value)) return new Set();
return new Set(value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0));
};
export const evaluateServerCompatibility = (
payload: ServerCompatibilityPayload | null | undefined,
options: {
clientApiVersion?: number;
requiredCapabilities?: readonly string[];
} = {},
): ServerCompatibilityResult => {
const clientApiVersion = options.clientApiVersion ?? OPENCHAMBER_CLIENT_API_VERSION;
const requiredCapabilities = [...(options.requiredCapabilities ?? REQUIRED_SERVER_CAPABILITIES)];
const compatibility = payload?.compatibility ?? null;
const apiVersion = parsePositiveInteger(compatibility?.apiVersion);
const minClientApiVersion = parsePositiveInteger(compatibility?.minClientApiVersion);
const openchamberVersion = parseString(payload?.openchamberVersion);
const runtime = parseString(payload?.runtime);
const base = {
openchamberVersion,
runtime,
apiVersion,
minClientApiVersion,
missingCapabilities: [] as string[],
requiredCapabilities,
};
if (!payload || payload.status !== 'ok' || !compatibility || !apiVersion || !minClientApiVersion) {
return {
...base,
status: 'invalid-response',
message: 'Server did not return OpenChamber compatibility metadata.',
};
}
if (apiVersion < clientApiVersion) {
return {
...base,
status: 'server-too-old',
message: `Server API version ${apiVersion} is older than required client API version ${clientApiVersion}.`,
};
}
if (minClientApiVersion > clientApiVersion) {
return {
...base,
status: 'client-too-old',
message: `Server requires client API version ${minClientApiVersion}, but this client supports ${clientApiVersion}.`,
};
}
const capabilities = parseCapabilities(compatibility.capabilities);
const missingCapabilities = requiredCapabilities.filter((capability) => !capabilities.has(capability));
if (missingCapabilities.length > 0) {
return {
...base,
status: 'missing-capability',
missingCapabilities,
message: `Server is missing required capabilities: ${missingCapabilities.join(', ')}.`,
};
}
return {
...base,
status: 'compatible',
message: 'Server is compatible.',
};
};
export const checkServerCompatibility = async (): Promise<ServerCompatibilityResult> => {
let response: Response;
try {
response = await runtimeFetch('/api/version', {
method: 'GET',
headers: { Accept: 'application/json' },
});
} catch (error) {
return {
status: 'unreachable',
openchamberVersion: null,
runtime: null,
apiVersion: null,
minClientApiVersion: null,
missingCapabilities: [],
requiredCapabilities: [...REQUIRED_SERVER_CAPABILITIES],
message: error instanceof Error ? error.message : 'Server is unreachable.',
};
}
if (response.status === 401 || response.status === 403) {
return {
status: 'auth-required',
openchamberVersion: null,
runtime: null,
apiVersion: null,
minClientApiVersion: null,
missingCapabilities: [],
requiredCapabilities: [...REQUIRED_SERVER_CAPABILITIES],
message: 'Server requires authentication.',
};
}
const payload = await response.json().catch(() => null) as ServerCompatibilityPayload | null;
return evaluateServerCompatibility(payload);
};
+2 -13
View File
@@ -25,7 +25,7 @@ export type SettingsPageSlug =
| 'tunnel'
| 'about';
export type SettingsPageGroup =
type SettingsPageGroup =
| 'appearance'
| 'projects'
| 'general'
@@ -52,17 +52,6 @@ export interface SettingsPageMeta {
isAvailable?: (ctx: SettingsRuntimeContext) => boolean;
}
export const SETTINGS_GROUP_LABELS: Record<SettingsPageGroup, string> = {
appearance: 'Appearance',
projects: 'Projects',
general: 'General',
opencode: 'OpenCode',
git: 'Git',
skills: 'Skills',
usage: 'Usage',
advanced: 'Advanced',
};
export const SETTINGS_PAGE_METADATA: readonly SettingsPageMeta[] = [
{
slug: 'home',
@@ -209,7 +198,7 @@ export const SETTINGS_PAGE_METADATA: readonly SettingsPageMeta[] = [
{ slug: 'about', title: 'About', group: 'advanced', kind: 'single', keywords: ['about', 'version', 'updates', 'release', 'changelog'], isAvailable: (ctx) => ctx.isMobile },
] as const;
export const LEGACY_SIDEBAR_SECTION_TO_SETTINGS_SLUG: Record<SidebarSection, SettingsPageSlug> = {
const LEGACY_SIDEBAR_SECTION_TO_SETTINGS_SLUG: Record<SidebarSection, SettingsPageSlug> = {
sessions: 'sessions',
agents: 'agents',
commands: 'commands',
+3 -3
View File
@@ -2,7 +2,7 @@ import type { I18nKey } from '@/lib/i18n/store';
import type { SettingsPageSlug, SettingsRuntimeContext } from './metadata';
import { getSettingsPageMeta } from './metadata';
export interface SettingsSearchItem {
interface SettingsSearchItem {
id: string;
page: SettingsPageSlug;
titleKey: I18nKey;
@@ -17,12 +17,12 @@ export interface SettingsSearchResult extends SettingsSearchItem {
pageTitle: string;
}
export interface SettingsSearchAvailabilityContext extends SettingsRuntimeContext {
interface SettingsSearchAvailabilityContext extends SettingsRuntimeContext {
isMobile: boolean;
isDesktopLocalOrigin: boolean;
}
export const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
{
id: 'appearance.language',
page: 'appearance',
+5 -28
View File
@@ -1,8 +1,8 @@
import { isMacOS } from '@/lib/utils';
import { isDesktopShell } from '@/lib/desktop';
export type ShortcutModifier = 'mod' | 'shift' | 'alt' | 'option' | 'ctrl';
export type ShortcutKey = string;
type ShortcutModifier = 'mod' | 'shift' | 'alt' | 'option' | 'ctrl';
type ShortcutKey = string;
export type ShortcutCombo = string;
export const UNASSIGNED_SHORTCUT: ShortcutCombo = '__unassigned__';
@@ -15,7 +15,7 @@ export interface ShortcutAction {
customizable?: boolean;
}
export interface ParsedShortcut {
interface ParsedShortcut {
modifiers: Set<ShortcutModifier>;
key: ShortcutKey;
}
@@ -417,7 +417,7 @@ export function normalizeCombo(combo: ShortcutCombo): ShortcutCombo {
return [...orderedModifiers, key].filter(Boolean).join('+');
}
export function isValidShortcutCombo(combo: ShortcutCombo): boolean {
function isValidShortcutCombo(combo: ShortcutCombo): boolean {
if (isUnassignedShortcut(combo)) {
return true;
}
@@ -426,7 +426,7 @@ export function isValidShortcutCombo(combo: ShortcutCombo): boolean {
return parsed.key.trim().length > 0;
}
export function parseShortcut(combo: ShortcutCombo): ParsedShortcut {
function parseShortcut(combo: ShortcutCombo): ParsedShortcut {
if (isUnassignedShortcut(combo)) {
return { modifiers: new Set<ShortcutModifier>(), key: UNASSIGNED_SHORTCUT };
}
@@ -479,10 +479,6 @@ export function getShortcutAction(id: string): ShortcutAction | undefined {
return SHORTCUT_ACTIONS.find((action) => action.id === id);
}
export function getAllShortcutActions(): ReadonlyArray<ShortcutAction> {
return SHORTCUT_ACTIONS;
}
export function getCustomizableShortcutActions(): ReadonlyArray<ShortcutAction> {
return SHORTCUT_ACTIONS.filter((action) => action.customizable === true);
}
@@ -515,17 +511,6 @@ export function getEffectiveShortcutCombo(
return action.defaultCombo;
}
export function getEffectiveShortcutLabel(
actionId: string,
overrides?: Record<string, ShortcutCombo>
): string {
const combo = getEffectiveShortcutCombo(actionId, overrides);
if (!combo) {
return '';
}
return formatShortcutForDisplay(combo);
}
export function isRiskyBrowserShortcut(combo: ShortcutCombo): boolean {
if (isUnassignedShortcut(combo)) {
return false;
@@ -607,14 +592,6 @@ export function eventMatchesShortcut(
return eventKey === expectedKey;
}
export function getShortcutLabel(id: string): string {
const action = getShortcutAction(id);
if (!action) return '';
const displayCombo = formatShortcutForDisplay(action.defaultCombo);
return `${displayCombo} - ${action.label}`;
}
export function getModifierLabel(): string {
return isMacOS() && isDesktopShell() ? '⌘' : 'Ctrl';
}
+2 -2
View File
@@ -1,4 +1,4 @@
export type StartupTraceEvent = {
type StartupTraceEvent = {
t: number;
name: string;
data?: Record<string, unknown>;
@@ -43,7 +43,7 @@ export const markStartupTrace = (name: string, data?: Record<string, unknown>) =
}
};
export const getStartupTraceSummary = () => {
const getStartupTraceSummary = () => {
const trace = typeof window !== 'undefined' ? window.__OPENCHAMBER_STARTUP_TRACE__ ?? [] : [];
const readyIndex = trace.findIndex((event) => event.name === 'ModelControls:ready');
const endIndex = readyIndex >= 0 ? Math.min(trace.length, readyIndex + 8) : trace.length;
@@ -1,497 +0,0 @@
/**
* SerializeAddon for ghostty-web
*
* Port of xterm.js addon-serialize for ghostty-web terminal.
* Enables serialization of terminal contents to restore state after reconnection.
*
* Features:
* - ANSI color preservation (16-color, 256-color, RGB)
* - Text attributes (bold, italic, underline, faint, strikethrough, blink, inverse, invisible, dim)
* - Scrollback support with configurable limits
* - Round-trip compatibility
* - Cursor positioning
*/
import type { Terminal as GhosttyTerminal } from 'ghostty-web';
// Constants for ANSI escape codes
const C0 = {
ESC: '\u001b',
};
const SGR = {
RESET: 0,
BOLD: 1,
DIM: 2,
ITALIC: 3,
UNDERLINE: 4,
SLOW_BLINK: 5,
RAPID_BLINK: 6,
INVERSE: 7,
INVISIBLE: 8,
STRIKETHROUGH: 9,
NORMAL_INTENSITY: 22,
NO_ITALIC: 23,
NO_UNDERLINE: 24,
NO_BLINK: 25,
NO_INVERSE: 27,
VISIBLE: 28,
NO_STRIKETHROUGH: 29,
FG_DEFAULT: 39,
BG_DEFAULT: 49,
};
export interface SerializeOptions {
/**
* The row range to serialize. When an explicit range is specified, the cursor
* will get its final repositioning.
*/
range?: {
start: number;
end: number;
};
/**
* The number of rows in the scrollback buffer to serialize, starting from
* the bottom of the scrollback buffer. When not specified, all available
* rows in the scrollback buffer will be serialized.
*/
scrollback?: number;
/**
* Whether to exclude the terminal modes from the serialization.
* Default: false
*/
excludeModes?: boolean;
/**
* Whether to exclude the alt buffer from the serialization.
* Default: false
*/
excludeAltBuffer?: boolean;
}
export interface TextSerializeOptions {
/**
* The number of rows in the scrollback buffer to serialize, starting from
* the bottom of the scrollback buffer.
*/
scrollback?: number;
/**
* Whether to trim trailing whitespace from lines.
* Default: true
*/
trimWhitespace?: boolean;
}
interface CellState {
fg: number | null;
bg: number | null;
bold: boolean;
dim: boolean;
italic: boolean;
underline: boolean;
blink: boolean;
inverse: boolean;
invisible: boolean;
strikethrough: boolean;
}
const NULL_CELL_STATE: CellState = {
fg: null,
bg: null,
bold: false,
dim: false,
italic: false,
underline: false,
blink: false,
inverse: false,
invisible: false,
strikethrough: false,
};
/**
* SerializeAddon for ghostty-web terminal
*/
export class SerializeAddon {
private _terminal: GhosttyTerminal | undefined;
/**
* Activate the addon
*/
activate(terminal: GhosttyTerminal): void {
this._terminal = terminal;
}
/**
* Dispose the addon
*/
dispose(): void {
this._terminal = undefined;
}
/**
* Serialize the terminal buffer to ANSI escape sequences
*/
serialize(options: SerializeOptions = {}): string {
if (!this._terminal) {
throw new Error('SerializeAddon not activated');
}
const buffer = this._terminal.buffer.active;
if (!buffer) {
return '';
}
const result: string[] = [];
let currentState: CellState = { ...NULL_CELL_STATE };
// Determine range to serialize
const scrollbackLimit = options.scrollback ?? buffer.length;
let startRow: number;
let endRow: number;
if (options.range) {
startRow = options.range.start;
endRow = options.range.end;
} else {
// Serialize scrollback + viewport
const totalRows = buffer.length;
const scrollbackRows = Math.min(scrollbackLimit, totalRows - buffer.baseY);
startRow = Math.max(0, buffer.baseY - scrollbackRows);
endRow = buffer.baseY + buffer.cursorY;
}
// Clamp to valid range
startRow = Math.max(0, startRow);
endRow = Math.min(buffer.length - 1, endRow);
for (let y = startRow; y <= endRow; y++) {
const line = buffer.getLine(y);
if (!line) {
result.push('\r\n');
continue;
}
let lineContent = '';
let lastNonSpaceCol = -1;
// Find the last non-space column
for (let x = line.length - 1; x >= 0; x--) {
const cell = line.getCell(x);
if (cell) {
const char = this._getCellChar(cell);
if (char !== ' ' && char !== '') {
lastNonSpaceCol = x;
break;
}
}
}
// Serialize each cell up to the last non-space
for (let x = 0; x <= lastNonSpaceCol; x++) {
const cell = line.getCell(x);
if (!cell) {
lineContent += ' ';
continue;
}
// Get cell attributes and generate SGR sequences if needed
const newState = this._getCellState(cell);
const sgrSequences = this._generateSgrDiff(currentState, newState);
if (sgrSequences) {
lineContent += sgrSequences;
currentState = newState;
}
// Get character
const char = this._getCellChar(cell);
lineContent += char || ' ';
}
// Reset attributes at end of line if any were set
if (this._hasAttributes(currentState)) {
lineContent += `${C0.ESC}[${SGR.RESET}m`;
currentState = { ...NULL_CELL_STATE };
}
result.push(lineContent);
// Add newline unless it's the last row with cursor
if (y < endRow) {
result.push('\r\n');
}
}
// Position cursor
const cursorY = buffer.cursorY;
const cursorX = buffer.cursorX;
if (cursorY >= 0 && cursorX >= 0) {
// Use CUP (Cursor Position) to move cursor to correct position
// CUP is 1-based, so add 1 to both coordinates
const relativeY = cursorY - (endRow - buffer.baseY);
if (relativeY !== 0 || cursorX !== 0) {
result.push(`${C0.ESC}[${cursorY + 1};${cursorX + 1}H`);
}
}
return result.join('');
}
/**
* Serialize the terminal buffer to plain text (no escape sequences)
*/
serializeAsText(options: TextSerializeOptions = {}): string {
if (!this._terminal) {
throw new Error('SerializeAddon not activated');
}
const buffer = this._terminal.buffer.active;
if (!buffer) {
return '';
}
const trimWhitespace = options.trimWhitespace ?? true;
const scrollbackLimit = options.scrollback ?? buffer.length;
const result: string[] = [];
// Determine range
const totalRows = buffer.length;
const scrollbackRows = Math.min(scrollbackLimit, totalRows - buffer.baseY);
const startRow = Math.max(0, buffer.baseY - scrollbackRows);
const endRow = buffer.baseY + buffer.cursorY;
for (let y = startRow; y <= endRow; y++) {
const line = buffer.getLine(y);
if (!line) {
result.push('');
continue;
}
let lineContent = '';
for (let x = 0; x < line.length; x++) {
const cell = line.getCell(x);
if (cell) {
const char = this._getCellChar(cell);
lineContent += char || ' ';
} else {
lineContent += ' ';
}
}
if (trimWhitespace) {
lineContent = lineContent.trimEnd();
}
result.push(lineContent);
}
return result.join('\n');
}
/**
* Get the character from a cell, handling wide characters and special codepoints
*/
private _getCellChar(cell: { getChars?: () => string; getCodepoint?: () => number }): string {
// Try getChars() first (ghostty-web standard)
if (typeof cell.getChars === 'function') {
const chars = cell.getChars();
if (chars) return chars;
}
// Try getCodepoint()
if (typeof cell.getCodepoint === 'function') {
const codepoint = cell.getCodepoint();
if (codepoint && codepoint > 0 && codepoint <= 0x10FFFF &&
!(codepoint >= 0xD800 && codepoint <= 0xDFFF)) {
return String.fromCodePoint(codepoint);
}
}
// Fallback
return ' ';
}
/**
* Get the state of a cell (colors and attributes)
*/
private _getCellState(cell: {
getFgColor?: () => number;
getBgColor?: () => number;
isBold?: () => boolean | number;
isDim?: () => boolean | number;
isFaint?: () => boolean | number;
isItalic?: () => boolean | number;
isUnderline?: () => boolean | number;
isBlink?: () => boolean | number;
isInverse?: () => boolean | number;
isInvisible?: () => boolean | number;
isStrikethrough?: () => boolean | number;
}): CellState {
const state: CellState = { ...NULL_CELL_STATE };
// Get foreground color
if (typeof cell.getFgColor === 'function') {
const fg = cell.getFgColor();
if (fg !== undefined && fg !== null && fg !== -1) {
state.fg = fg;
}
}
// Get background color
if (typeof cell.getBgColor === 'function') {
const bg = cell.getBgColor();
if (bg !== undefined && bg !== null && bg !== -1) {
state.bg = bg;
}
}
// Get attributes
if (typeof cell.isBold === 'function') {
state.bold = !!cell.isBold();
}
if (typeof cell.isDim === 'function') {
state.dim = !!cell.isDim();
} else if (typeof cell.isFaint === 'function') {
state.dim = !!cell.isFaint();
}
if (typeof cell.isItalic === 'function') {
state.italic = !!cell.isItalic();
}
if (typeof cell.isUnderline === 'function') {
state.underline = !!cell.isUnderline();
}
if (typeof cell.isBlink === 'function') {
state.blink = !!cell.isBlink();
}
if (typeof cell.isInverse === 'function') {
state.inverse = !!cell.isInverse();
}
if (typeof cell.isInvisible === 'function') {
state.invisible = !!cell.isInvisible();
}
if (typeof cell.isStrikethrough === 'function') {
state.strikethrough = !!cell.isStrikethrough();
}
return state;
}
/**
* Generate SGR escape sequences for the difference between two cell states
*/
private _generateSgrDiff(from: CellState, to: CellState): string | null {
const codes: number[] = [];
// Check if we need a full reset
const needsReset =
(from.bold && !to.bold) ||
(from.dim && !to.dim) ||
(from.italic && !to.italic) ||
(from.underline && !to.underline) ||
(from.blink && !to.blink) ||
(from.inverse && !to.inverse) ||
(from.invisible && !to.invisible) ||
(from.strikethrough && !to.strikethrough);
if (needsReset) {
codes.push(SGR.RESET);
// After reset, we need to re-apply all 'to' attributes
if (to.bold) codes.push(SGR.BOLD);
if (to.dim) codes.push(SGR.DIM);
if (to.italic) codes.push(SGR.ITALIC);
if (to.underline) codes.push(SGR.UNDERLINE);
if (to.blink) codes.push(SGR.SLOW_BLINK);
if (to.inverse) codes.push(SGR.INVERSE);
if (to.invisible) codes.push(SGR.INVISIBLE);
if (to.strikethrough) codes.push(SGR.STRIKETHROUGH);
// Re-apply colors
if (to.fg !== null) {
this._appendColorCode(codes, to.fg, true);
}
if (to.bg !== null) {
this._appendColorCode(codes, to.bg, false);
}
} else {
// Apply only changed attributes
if (!from.bold && to.bold) codes.push(SGR.BOLD);
if (!from.dim && to.dim) codes.push(SGR.DIM);
if (!from.italic && to.italic) codes.push(SGR.ITALIC);
if (!from.underline && to.underline) codes.push(SGR.UNDERLINE);
if (!from.blink && to.blink) codes.push(SGR.SLOW_BLINK);
if (!from.inverse && to.inverse) codes.push(SGR.INVERSE);
if (!from.invisible && to.invisible) codes.push(SGR.INVISIBLE);
if (!from.strikethrough && to.strikethrough) codes.push(SGR.STRIKETHROUGH);
// Handle color changes
if (from.fg !== to.fg) {
if (to.fg === null) {
codes.push(SGR.FG_DEFAULT);
} else {
this._appendColorCode(codes, to.fg, true);
}
}
if (from.bg !== to.bg) {
if (to.bg === null) {
codes.push(SGR.BG_DEFAULT);
} else {
this._appendColorCode(codes, to.bg, false);
}
}
}
if (codes.length === 0) {
return null;
}
return `${C0.ESC}[${codes.join(';')}m`;
}
/**
* Append color code to the codes array
*/
private _appendColorCode(codes: number[], color: number, isForeground: boolean): void {
const base = isForeground ? 30 : 40;
const extBase = isForeground ? 38 : 48;
if (color < 8) {
// Basic 8 colors
codes.push(base + color);
} else if (color < 16) {
// Bright 8 colors
codes.push(base + 60 + (color - 8));
} else if (color < 256) {
// 256-color palette
codes.push(extBase, 5, color);
} else {
// RGB (24-bit) color encoded as 0xRRGGBB + 0x1000000
const rgb = color - 0x1000000;
const r = (rgb >> 16) & 0xFF;
const g = (rgb >> 8) & 0xFF;
const b = rgb & 0xFF;
codes.push(extBase, 2, r, g, b);
}
}
/**
* Check if the state has any attributes set
*/
private _hasAttributes(state: CellState): boolean {
return (
state.fg !== null ||
state.bg !== null ||
state.bold ||
state.dim ||
state.italic ||
state.underline ||
state.blink ||
state.inverse ||
state.invisible ||
state.strikethrough
);
}
}
+2 -2
View File
@@ -1,13 +1,13 @@
import { getRuntimeUrlResolver } from './runtime-url';
import { runtimeFetch } from './runtime-fetch';
export interface TerminalWebSocketDescriptor {
interface TerminalWebSocketDescriptor {
path: string;
v?: number;
enc?: string;
}
export interface TerminalTransportCapability {
interface TerminalTransportCapability {
preferred?: 'ws' | 'http' | 'sse';
transports?: Array<'ws' | 'http' | 'sse'>;
ws?: TerminalWebSocketDescriptor;
-29
View File
@@ -62,35 +62,6 @@ export function convertThemeToXterm(theme: Theme): TerminalTheme {
};
}
export function getTerminalOptions(
fontFamily: string,
fontSize: number,
theme: TerminalTheme
) {
const powerlineFallbacks =
'"JetBrainsMonoNL Nerd Font", "FiraCode Nerd Font", "Cascadia Code PL", "Fira Code", "JetBrains Mono", "SFMono-Regular", Menlo, Consolas, "Liberation Mono", "Courier New", monospace';
const augmentedFontFamily = `${fontFamily}, ${powerlineFallbacks}`;
return {
fontFamily: augmentedFontFamily,
fontSize,
lineHeight: 1,
cursorBlink: false,
cursorStyle: 'bar' as const,
theme,
allowTransparency: false,
scrollback: 10_000,
minimumContrastRatio: 1,
fastScrollModifier: 'shift' as const,
fastScrollSensitivity: 5,
scrollSensitivity: 3,
macOptionIsMeta: true,
macOptionClickForcesSelection: false,
rightClickSelectsWord: true,
};
}
/**
* Get terminal options for Ghostty Web terminal
*/
+4 -4
View File
@@ -6,10 +6,10 @@ import flexokiDarkRaw from './flexoki-dark.json';
import openchamberLightRaw from './fields-of-the-shire-light.json';
import openchamberDarkRaw from './fields-of-the-shire-dark.json';
export const flexokiLightTheme = withPrColors(flexokiLightRaw as Theme);
export const flexokiDarkTheme = withPrColors(flexokiDarkRaw as Theme);
export const openchamberLightTheme = withPrColors(openchamberLightRaw as Theme);
export const openchamberDarkTheme = withPrColors(openchamberDarkRaw as Theme);
const flexokiLightTheme = withPrColors(flexokiLightRaw as Theme);
const flexokiDarkTheme = withPrColors(flexokiDarkRaw as Theme);
const openchamberLightTheme = withPrColors(openchamberLightRaw as Theme);
const openchamberDarkTheme = withPrColors(openchamberDarkRaw as Theme);
export const DEFAULT_LIGHT_THEME_ID = 'flexoki-light' as const;
export const DEFAULT_DARK_THEME_ID = 'flexoki-dark' as const;
+1 -1
View File
@@ -4,7 +4,7 @@ import { getDefaultTheme } from '@/lib/theme/themes';
export type VSCodeThemeKind = 'light' | 'dark' | 'high-contrast';
export type VSCodeThemeColorToken =
type VSCodeThemeColorToken =
// Editor core
| 'editor.background'
| 'editor.foreground'
-15
View File
@@ -9,21 +9,6 @@ const getHour12Option = (preference: TimeFormatPreference): boolean | undefined
return undefined;
};
export const getUses24HourForPreference = (preference: TimeFormatPreference, locale: string): boolean => {
if (preference === '24h') return true;
if (preference === '12h') return false;
try {
const options = new Intl.DateTimeFormat(locale, { hour: 'numeric' }).resolvedOptions();
if (typeof options.hour12 === 'boolean') {
return !options.hour12;
}
return options.hourCycle === 'h23' || options.hourCycle === 'h24';
} catch {
return true;
}
};
export const formatTimeForPreference = (
timestamp: number | Date,
preference: TimeFormatPreference,
+1 -1
View File
@@ -11,7 +11,7 @@ export interface ToolMetadata {
category: 'file' | 'search' | 'code' | 'system' | 'ai' | 'web';
}
export const TOOL_METADATA: Record<string, ToolMetadata> = {
const TOOL_METADATA: Record<string, ToolMetadata> = {
read: {
displayName: 'Read File',
-83
View File
@@ -1,83 +0,0 @@
const ACTIVE_TOOL_STATUSES = new Set([
'pending',
'running',
'started',
'inprogress',
'processing',
'executing',
]);
const FINAL_TOOL_STATUSES = new Set([
'completed',
'complete',
'error',
'failed',
'aborted',
'timeout',
'timedout',
'done',
'cancelled',
'canceled',
]);
const readTimestamp = (value: unknown): number | undefined => {
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
};
export const normalizeToolStatus = (status: unknown): string | undefined => {
if (typeof status !== 'string') {
return undefined;
}
const normalized = status.toLowerCase().trim().replace(/[\s_-]+/g, '');
return normalized.length > 0 ? normalized : undefined;
};
export const isActiveToolStatus = (status: unknown): boolean => {
const normalized = normalizeToolStatus(status);
return normalized ? ACTIVE_TOOL_STATUSES.has(normalized) : false;
};
export const isFinalToolStatus = (status: unknown): boolean => {
const normalized = normalizeToolStatus(status);
return normalized ? FINAL_TOOL_STATUSES.has(normalized) : false;
};
export type ToolLifecycleState = {
status?: string;
start?: number;
end?: number;
hasStarted: boolean;
hasEnded: boolean;
isStatusActive: boolean;
isStatusFinal: boolean;
isInFlight: boolean;
isFinalized: boolean;
};
export const getToolLifecycleState = (
state?: { status?: unknown; time?: { start?: unknown; end?: unknown } } | null,
): ToolLifecycleState => {
const status = normalizeToolStatus(state?.status);
const start = readTimestamp(state?.time?.start);
const rawEnd = readTimestamp(state?.time?.end);
const hasEnded = typeof rawEnd === 'number' && (typeof start !== 'number' || rawEnd >= start);
const end = hasEnded ? rawEnd : undefined;
const isStatusActive = status ? ACTIVE_TOOL_STATUSES.has(status) : false;
const isStatusFinal = status ? FINAL_TOOL_STATUSES.has(status) : false;
const isUnknownNonFinal = status ? !isStatusActive && !isStatusFinal : true;
const isInFlight = !hasEnded && (isStatusActive || isUnknownNonFinal);
const isFinalized = hasEnded || isStatusFinal;
return {
status,
start,
end,
hasStarted: typeof start === 'number',
hasEnded,
isStatusActive,
isStatusFinal,
isInFlight,
isFinalized,
};
};
-107
View File
@@ -7,28 +7,6 @@ export const SEMANTIC_TYPOGRAPHY = {
micro: '0.875rem',
} as const;
export const FONT_SIZE_SCALES = {
small: {
markdown: '0.875rem',
code: '0.8125rem',
uiHeader: '0.875rem',
uiLabel: '0.8125rem',
meta: '0.8125rem',
micro: '0.75rem',
},
medium: SEMANTIC_TYPOGRAPHY,
large: {
markdown: '1rem',
code: '0.9375rem',
uiHeader: '1rem',
uiLabel: '0.9375rem',
meta: '0.9375rem',
micro: '0.9375rem',
},
} as const;
export type FontSizeOption = keyof typeof FONT_SIZE_SCALES;
export const VSCODE_TYPOGRAPHY = {
// Keep VS Code webview typography slightly tighter; VS Code UI chrome already provides density.
markdown: '0.9063rem',
@@ -39,35 +17,12 @@ export const VSCODE_TYPOGRAPHY = {
micro: '0.7813rem',
} as const;
export const SEMANTIC_TYPOGRAPHY_CSS = {
'--text-markdown': SEMANTIC_TYPOGRAPHY.markdown,
'--text-code': SEMANTIC_TYPOGRAPHY.code,
'--text-ui-header': SEMANTIC_TYPOGRAPHY.uiHeader,
'--text-ui-label': SEMANTIC_TYPOGRAPHY.uiLabel,
'--text-meta': SEMANTIC_TYPOGRAPHY.meta,
'--text-micro': SEMANTIC_TYPOGRAPHY.micro,
} as const;
export const TYPOGRAPHY_CLASSES = {
markdown: 'typography-markdown',
code: 'typography-code',
uiHeader: 'typography-ui-header',
uiLabel: 'typography-ui-label',
meta: 'typography-meta',
micro: 'typography-micro',
} as const;
export type SemanticTypographyKey = keyof typeof SEMANTIC_TYPOGRAPHY;
export type TypographyClassKey = keyof typeof TYPOGRAPHY_CLASSES;
export function getTypographyVariable(key: SemanticTypographyKey): string {
return `--text-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
}
export function getTypographyClass(key: TypographyClassKey): string {
return TYPOGRAPHY_CLASSES[key];
}
export const typography = {
semanticMarkdown: {
@@ -248,22 +203,6 @@ export const typography = {
},
};
export function getTypographyStyle(path: string, fallback?: React.CSSProperties): React.CSSProperties {
const parts = path.split('.');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let current: any = typography;
for (const part of parts) {
if (current && current[part]) {
current = current[part];
} else {
return fallback || {};
}
}
return current || fallback || {};
}
export const toolDisplayStyles = {
padding: {
@@ -309,49 +248,3 @@ export const toolDisplayStyles = {
...typography.tool.inline,
}),
};
export const typographyClasses = {
'heading-1': 'typography-h1',
'heading-2': 'typography-h2',
'heading-3': 'typography-h3',
'heading-4': 'typography-h4',
'heading-5': 'typography-h5',
'heading-6': 'typography-h6',
'ui-button': 'typography-ui-button',
'ui-button-small': 'typography-ui-button-small',
'ui-button-large': 'typography-ui-button-large',
'ui-label': 'typography-ui-label',
'ui-caption': 'typography-ui-caption',
'ui-badge': 'typography-ui-badge',
'ui-tooltip': 'typography-ui-tooltip',
'ui-input': 'typography-ui-input',
'ui-helper': 'typography-ui-helper-text',
'code-inline': 'typography-code-inline',
'code-block': 'typography-code-block',
'code-line-numbers': 'typography-code-line-numbers',
'markdown-h1': 'typography-markdown-h1',
'markdown-h2': 'typography-markdown-h2',
'markdown-h3': 'typography-markdown-h3',
'markdown-h4': 'typography-markdown-h4',
'markdown-h5': 'typography-markdown-h5',
'markdown-h6': 'typography-markdown-h6',
'markdown-body': 'typography-markdown-body',
'markdown-body-small': 'typography-markdown-body-small',
'markdown-body-large': 'typography-markdown-body-large',
'markdown-blockquote': 'typography-markdown-blockquote',
'markdown-list': 'typography-markdown-list',
'markdown-link': 'typography-markdown-link',
'markdown-code': 'typography-markdown-code',
'markdown-code-block': 'typography-markdown-code-block',
'semantic-markdown': 'typography-markdown',
'semantic-code': 'typography-code',
'semantic-ui-header': 'typography-ui-header',
'semantic-ui-label': 'typography-ui-label',
'semantic-meta': 'typography-meta',
'semantic-micro': 'typography-micro',
};
+1 -1
View File
@@ -17,7 +17,7 @@ export const isMacOS = (): boolean => {
return /Macintosh|Mac OS X/.test(navigator.userAgent || '');
};
export const isWindows = (): boolean => {
const isWindows = (): boolean => {
if (typeof navigator === 'undefined') return false;
return /Windows/.test(navigator.userAgent || '');
};
@@ -20,10 +20,10 @@
import { runtimeFetch } from '@/lib/runtime-fetch';
export type SpeechResultCallback = (text: string, isFinal: boolean) => void;
export type ErrorCallback = (error: string) => void;
type SpeechResultCallback = (text: string, isFinal: boolean) => void;
type ErrorCallback = (error: string) => void;
export interface AudioStreamConfig {
interface AudioStreamConfig {
/** Base URL of the OpenAI-compatible STT server (e.g. http://localhost:8001/v1) */
baseURL: string;
/** Whisper-compatible model name */
@@ -395,4 +395,3 @@ class AudioStreamService {
}
export const audioStreamService = new AudioStreamService();
export { AudioStreamService };
@@ -34,9 +34,9 @@ declare global {
}
// Callback types
export type SpeechResultCallback = (text: string, isFinal: boolean) => void;
export type SpeechEndCallback = () => void;
export type ErrorCallback = (error: string) => void;
type SpeechResultCallback = (text: string, isFinal: boolean) => void;
type SpeechEndCallback = () => void;
type ErrorCallback = (error: string) => void;
/**
* Browser Voice Service class
@@ -234,7 +234,7 @@ class BrowserVoiceService {
this.isListening = true;
this.restartOnEnd = true;
};
this.recognition.onaudiostart = () => {
console.log('[BrowserVoiceService] Audio recording started');
};
@@ -652,6 +652,3 @@ class BrowserVoiceService {
// Export singleton instance
export const browserVoiceService = new BrowserVoiceService();
// Also export the class for testing/customization
export { BrowserVoiceService };
@@ -29,7 +29,7 @@ export interface VoiceMessage {
* @param message - The message to format
* @returns Formatted text for voice, or null if should not be spoken
*/
export function formatMessage(message: VoiceMessage): string | null {
function formatMessage(message: VoiceMessage): string | null {
// Handle edge cases
if (!message || typeof message.content !== "string") {
return null;
+2 -21
View File
@@ -1,36 +1,17 @@
/**
* Voice module barrel export
* Provides clean import path for voice configuration and client tools
* Provides a clean import path for voice session hooks.
*
* @example
* ```typescript
* import { VOICE_CONFIG, realtimeClientTools, voiceHooks } from '@/lib/voice';
* import { voiceHooks } from '@/lib/voice';
* ```
*/
// Configuration
export { VOICE_CONFIG } from "./voiceConfig";
// Client tools for ElevenLabs voice agent
export { realtimeClientTools } from "./realtimeClientTools";
export type { RealtimeClientTools } from "./realtimeClientTools";
// Voice session registry (from voiceSession.ts)
export {
registerVoiceSession,
unregisterVoiceSession,
getVoiceSession,
isVoiceSessionStarted,
} from "./voiceSession";
// Voice hooks for session-to-voice event routing (from voiceHooks.ts)
export { voiceHooks } from "./voiceHooks";
// Context formatters for voice-native output
export {
formatMessage,
formatNewMessages,
formatPermissionRequest,
formatReadyEvent,
type VoiceMessage,
} from "./contextFormatters";
@@ -1,116 +0,0 @@
import { z } from "zod";
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useConfigStore } from "@/stores/useConfigStore";
import { getSyncPermissions } from "@/sync/sync-refs";
import { respondToPermission } from "@/sync/session-actions";
/**
* Static client tools for the realtime voice interface.
* These tools allow the voice agent to interact with Claude Code.
*/
export const realtimeClientTools = {
/**
* Send a message to Claude Code via the current session.
* Validates parameters with Zod and returns status strings.
*/
messageClaudeCode: async (parameters: unknown): Promise<string> => {
// Validate parameters with Zod
const schema = z.object({
message: z.string().min(1, "Message cannot be empty"),
});
const parsed = schema.safeParse(parameters);
if (!parsed.success) {
console.error("[Voice] Invalid message parameter:", parsed.error);
return "error (invalid message parameter)";
}
// Get current session ID from store
const sessionId = useSessionUIStore.getState().currentSessionId;
if (!sessionId) {
console.error("[Voice] No active session");
return "error (no active session)";
}
// Get current provider and model from config store
const { currentProviderId, currentModelId, currentAgentName, currentVariant } = useConfigStore.getState();
if (!currentProviderId || !currentModelId) {
console.error("[Voice] No provider/model selected");
return "error (no provider or model selected)";
}
try {
console.log("[Voice] Sending message to session:", sessionId);
await useSessionUIStore
.getState()
.sendMessage(
parsed.data.message,
currentProviderId,
currentModelId,
currentAgentName ?? undefined,
undefined,
undefined,
undefined,
currentVariant ?? undefined,
);
return "sent";
} catch (error) {
console.error("[Voice] Failed to send message:", error);
return "error (failed to send message)";
}
},
/**
* Process a permission request from voice.
* Validates decision with Zod enum and interacts with permission store.
*/
processPermissionRequest: async (parameters: unknown): Promise<string> => {
// Validate parameters with Zod
const schema = z.object({
decision: z.enum(["allow", "deny"]),
});
const parsed = schema.safeParse(parameters);
if (!parsed.success) {
console.error("[Voice] Invalid decision parameter:", parsed.error);
return "error (invalid decision parameter, expected 'allow' or 'deny')";
}
// Get current session ID from store
const sessionId = useSessionUIStore.getState().currentSessionId;
if (!sessionId) {
console.error("[Voice] No active session");
return "error (no active session)";
}
// Get pending permissions for this session
const permissions = getSyncPermissions(sessionId);
if (!permissions || permissions.length === 0) {
console.error("[Voice] No pending permission requests");
return "error (no pending permission request)";
}
// Get the first pending permission request
const request = permissions[0];
if (!request) {
return "error (no pending permission request)";
}
try {
const decision = parsed.data.decision;
console.log(`[Voice] Processing permission request ${request.id}: ${decision}`);
// Respond to the permission based on decision
const response: "once" | "always" | "reject" = decision === "allow" ? "once" : "reject";
await respondToPermission(sessionId, request.id, response);
return "done";
} catch (error) {
console.error("[Voice] Failed to process permission:", error);
return `error (failed to ${parsed.data.decision} permission)`;
}
},
};
/** Type for the realtime client tools */
export type RealtimeClientTools = typeof realtimeClientTools;
-111
View File
@@ -1,114 +1,3 @@
/**
* Text summarization utility
*
* Calls the server-side text summarization endpoint which uses
* the opencode.ai zen API with gpt-5-nano.
*/
import { useConfigStore } from '@/stores/useConfigStore';
import { runtimeFetch } from '@/lib/runtime-fetch';
/**
* Summarize text using the server-side zen API endpoint
*
* @param text - The text to summarize
* @param options - Optional configuration
* @returns The summarized text, or original text if summarization fails
*/
export async function summarizeText(
text: string,
options?: {
/** Character threshold - don't summarize if under this length */
threshold?: number;
/** Max characters for the summary output */
maxLength?: number;
/** Summarization mode */
mode?: 'tts' | 'note';
}
): Promise<string> {
const store = useConfigStore.getState();
const threshold = options?.threshold ?? store.summarizeCharacterThreshold;
const maxLength = options?.maxLength ?? store.summarizeMaxLength;
const mode = options?.mode ?? 'tts';
const normalizedSource = text.replace(/\s+/g, ' ').trim();
// Don't summarize if text is under threshold
if (text.length <= threshold) {
if (mode === 'note') {
throw new Error('Note summarization threshold bypass is not allowed');
}
return text;
}
try {
const response = await runtimeFetch('/api/text/summarize', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ text, threshold, maxLength, mode }),
});
if (!response.ok) {
const errorText = await response.text();
console.error(`[summarize] HTTP error ${response.status}:`, errorText);
throw new Error(`Summarization failed: ${response.status}`);
}
const data = await response.json() as {
summarized: boolean;
summary?: string;
reason?: string;
originalLength?: number;
summaryLength?: number;
};
if (typeof data.summary === 'string' && data.summary.trim().length > 0) {
const summary = data.summary.trim();
if (mode === 'note') {
const normalizedSummary = summary.replace(/\s+/g, ' ').trim();
if (normalizedSummary === normalizedSource) {
throw new Error('Note distillation returned source text unchanged');
}
}
return summary;
}
if (mode === 'note') {
throw new Error('Note summarization returned no distilled result');
}
// Return original text if the server produced nothing usable
return text;
} catch (err) {
console.error('[summarize] Failed to summarize:', err);
if (mode === 'note') {
throw err instanceof Error ? err : new Error('Note summarization failed');
}
// Return original text on error
return text;
}
}
/**
* Check if text should be summarized based on settings
*/
export function shouldSummarize(
text: string,
context: 'message' | 'voice'
): boolean {
const store = useConfigStore.getState();
const isEnabled = context === 'message'
? store.summarizeMessageTTS
: store.summarizeVoiceConversation;
if (!isEnabled) {
return false;
}
return text.length > store.summarizeCharacterThreshold;
}
/**
* Client-side text sanitization for TTS output.
* Removes markdown, URLs, file paths, and other non-speakable content.
-3
View File
@@ -30,6 +30,3 @@ export const VOICE_CONFIG = {
/** Enable debug logging for voice context updates */
ENABLE_DEBUG_LOGGING: true,
} as const;
/** Type for VOICE_CONFIG keys */
export type VoiceConfigKey = keyof typeof VOICE_CONFIG;
-8
View File
@@ -24,14 +24,6 @@ import {
} from "./contextFormatters";
import { getVoiceSession, isVoiceSessionStarted } from "./voiceSession";
// Re-export registry functions from voiceSession.ts for convenience
export {
registerVoiceSession,
unregisterVoiceSession,
getVoiceSession,
isVoiceSessionStarted,
} from "./voiceSession";
/**
* Report a contextual update to the voice session
* Internal helper that checks preconditions and handles errors
+1 -19
View File
@@ -10,25 +10,7 @@ interface VoiceSession {
* Global storage for the active voice session.
* Used by voiceHooks to send contextual updates to the voice agent.
*/
let activeVoiceSession: VoiceSession | null = null;
/**
* Register a voice session for use by voiceHooks.
* Called by useVoice when a conversation is established.
*/
export function registerVoiceSession(session: VoiceSession): void {
activeVoiceSession = session;
console.log("[Voice] Session registered");
}
/**
* Unregister the active voice session.
* Called by useVoice when the session ends.
*/
export function unregisterVoiceSession(): void {
activeVoiceSession = null;
console.log("[Voice] Session unregistered");
}
const activeVoiceSession: VoiceSession | null = null;
/**
* Get the currently registered voice session.
+2 -3
View File
@@ -48,8 +48,8 @@ export const WASM_MODELS: WasmModelInfo[] = [
},
];
export type SpeechResultCallback = (text: string, isFinal: boolean) => void;
export type ErrorCallback = (error: string) => void;
type SpeechResultCallback = (text: string, isFinal: boolean) => void;
type ErrorCallback = (error: string) => void;
const VAD_POLL_MS = 80;
const MIN_UTTERANCE_MS = 300;
@@ -553,4 +553,3 @@ class WasmSttService {
}
export const wasmSttService = new WasmSttService();
export { WasmSttService };
@@ -285,173 +285,10 @@ export async function createWorktreeSession(): Promise<string | null> {
/**
* Check if a worktree session is currently being created.
*/
export function isCreatingWorktree(): boolean {
return isCreatingWorktreeSession;
}
export async function createWorktreeDraft(options?: { initialPrompt?: string; title?: string }): Promise<string | null> {
return createInstantWorktreeDraft(options);
}
export async function createWorktreeOnly(): Promise<string | null> {
if (isCreatingWorktreeSession) {
return null;
}
const activeProject = useProjectsStore.getState().getActiveProject();
if (!activeProject?.path) {
toast.error('No active project', {
description: 'Please select a project first.',
});
return null;
}
const projectDirectory = activeProject.path;
let isGitRepo = false;
try {
isGitRepo = await checkIsGitRepository(projectDirectory);
} catch {
// ignored
}
if (!isGitRepo) {
toast.error('Not a Git repository', {
description: 'Worktrees can only be created in Git repositories.',
});
return null;
}
isCreatingWorktreeSession = true;
try {
const projectRef: ProjectRef = { id: activeProject.id, path: projectDirectory };
const preferredName = generateBranchName();
const setupCommands = await getWorktreeSetupCommands(projectRef);
const metadata = await createWorktreeWithDefaults(projectRef, {
preferredName,
mode: 'new',
branchName: preferredName,
worktreeName: preferredName,
setupCommands,
returnAfterDirectoryCreated: true,
});
return metadata.path;
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to create worktree';
toast.error('Failed to create worktree', {
description: message,
});
return null;
} finally {
isCreatingWorktreeSession = false;
}
}
/**
* Create a new session with a worktree for a specific branch.
* Unlike createWorktreeSession(), this allows specifying the project and branch explicitly.
*
* @param projectDirectory - The root directory of the git repository
* @param branchName - The name of the branch to create a worktree for
* @returns The created session, or null if creation failed
*/
export async function createWorktreeSessionForBranch(
projectDirectory: string,
branchName: string,
options?: {
kind?: 'pr' | 'standard';
existingBranch?: string;
worktreeName?: string;
setUpstream?: boolean;
upstreamRemote?: string;
upstreamBranch?: string;
ensureRemoteName?: string;
ensureRemoteUrl?: string;
createdFromBranch?: string;
returnAfterDirectoryCreated?: boolean;
}
): Promise<{ id: string } | null> {
if (isCreatingWorktreeSession) {
return null;
}
isCreatingWorktreeSession = true;
try {
const projectRef = resolveProjectRef(projectDirectory);
if (!projectRef) {
throw new Error('Project is not registered in OpenChamber');
}
// Check if it's a git repo (root project path)
let isGitRepo = false;
try {
isGitRepo = await checkIsGitRepository(projectRef.path);
} catch {
// Ignore errors, treat as not a git repo
}
if (!isGitRepo) {
toast.error('Not a Git repository', {
description: 'Worktrees can only be created in Git repositories.',
});
return null;
}
const setupCommands = await getWorktreeSetupCommands(projectRef);
const rootBranch = await getRootBranch(projectRef.path);
const metadata = await createWorktreeWithDefaults(projectRef, {
preferredName: branchName,
mode: 'existing',
existingBranch: options?.existingBranch || branchName,
branchName,
worktreeName: options?.worktreeName || branchName,
setUpstream: options?.setUpstream,
upstreamRemote: options?.upstreamRemote,
upstreamBranch: options?.upstreamBranch,
ensureRemoteName: options?.ensureRemoteName,
ensureRemoteUrl: options?.ensureRemoteUrl,
setupCommands,
returnAfterDirectoryCreated: options?.returnAfterDirectoryCreated,
});
const kind = options?.kind ?? 'standard';
const createdMetadata = {
...metadata,
createdFromBranch: options?.createdFromBranch || rootBranch,
kind,
};
await waitForWorktreeBootstrapIfEnabled(projectRef, metadata.path);
// Create the session
const sessionStore = useSessionUIStore.getState();
const session = await sessionStore.createSession(undefined, metadata.path);
if (!session) {
// Clean up the worktree if session creation failed
await removeProjectWorktree(projectRef, metadata, { deleteLocalBranch: true }).catch(() => undefined);
toast.error('Failed to create session', {
description: 'Could not create a session for the worktree.',
});
return null;
}
initializeSessionForWorktree(session.id, createdMetadata);
return session;
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to create worktree session';
toast.error('Failed to create worktree', {
description: message,
});
return null;
} finally {
isCreatingWorktreeSession = false;
}
}
/**
* Create a worktree session for a new branch name.
* Callers can still use startPoint for metadata or follow-up git operations.
@@ -550,36 +387,3 @@ export async function createWorktreeSessionForNewBranch(
isCreatingWorktreeSession = false;
}
}
/**
* Same as createWorktreeSessionForNewBranch, but preserves the exact branch name.
* Use when the worktree must be tied to a specific ref (e.g. PR head ref).
*/
export async function createWorktreeSessionForNewBranchExact(
projectDirectory: string,
branchName: string,
startPoint: string,
options?: {
kind?: 'pr' | 'standard';
worktreeName?: string;
setUpstream?: boolean;
upstreamRemote?: string;
upstreamBranch?: string;
ensureRemoteName?: string;
ensureRemoteUrl?: string;
createdFromBranch?: string;
returnAfterDirectoryCreated?: boolean;
}
): Promise<{ id: string; branch: string; path: string } | null> {
return createWorktreeSessionForNewBranch(projectDirectory, branchName, startPoint, {
kind: options?.kind,
worktreeName: options?.worktreeName,
setUpstream: options?.setUpstream,
upstreamRemote: options?.upstreamRemote,
upstreamBranch: options?.upstreamBranch,
ensureRemoteName: options?.ensureRemoteName,
ensureRemoteUrl: options?.ensureRemoteUrl,
createdFromBranch: options?.createdFromBranch,
returnAfterDirectoryCreated: options?.returnAfterDirectoryCreated,
});
}
@@ -65,7 +65,7 @@ export const resolveRootTrackingRemote = async (projectDirectory: string): Promi
return null;
};
export const resolveWorktreeUpstreamDefaults = async (
const resolveWorktreeUpstreamDefaults = async (
projectDirectory: string,
localBranch: string
): Promise<{ setUpstream: true; upstreamRemote: string; upstreamBranch: string } | null> => {
@@ -163,7 +163,7 @@ const deriveSdkWorktreeNameFromDirectory = (directory: string): string => {
return parts[parts.length - 1] ?? normalized;
};
export const buildSdkStartCommand = (args: {
const buildSdkStartCommand = (args: {
projectDirectory: string;
setupCommands: string[];
}): string | undefined => {
@@ -181,7 +181,7 @@ export const buildSdkStartCommand = (args: {
return joined.trim().length > 0 ? joined : undefined;
};
export const toCreatePayload = (args: {
const toCreatePayload = (args: {
preferredName?: string;
setupCommands?: string[];
mode?: 'new' | 'existing';
@@ -9,10 +9,6 @@ let statusImpl: (directory: string) => { current: string } = () => ({ current: '
const resolveRootCalls: string[] = [];
const statusCalls: string[] = [];
mock.module('@/lib/execCommands', () => ({
execCommands: () => Promise.resolve({ success: false, results: [] }),
}));
mock.module('@/lib/gitApi', () => ({
getGitStatus: (directory: string) => {
statusCalls.push(directory);