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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,6 @@ apps/site/node_modules/

# claude code session data
.claude/
app.log
app.pid
AGENTS.*
106 changes: 105 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 60 additions & 0 deletions src/__tests__/unit/claude-permissions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';

import {
DANGEROUSLY_SKIP_PERMISSIONS_UNSUPPORTED_CODE,
getDangerouslySkipPermissionsSupport,
isDangerouslySkipPermissionsSupported,
} from '../../lib/claude-permissions';

describe('claude-permissions', () => {
it('allows auto-approve for non-root users on linux', () => {
const result = getDangerouslySkipPermissionsSupport({
platform: 'linux',
uid: 1000,
env: {} as NodeJS.ProcessEnv,
});

assert.equal(result.supported, true);
assert.equal(
isDangerouslySkipPermissionsSupported({
platform: 'linux',
uid: 1000,
env: {} as NodeJS.ProcessEnv,
}),
true,
);
});

it('blocks auto-approve for unsandboxed root on linux', () => {
const result = getDangerouslySkipPermissionsSupport({
platform: 'linux',
uid: 0,
env: {} as NodeJS.ProcessEnv,
});

assert.equal(result.supported, false);
assert.equal(result.reasonCode, DANGEROUSLY_SKIP_PERMISSIONS_UNSUPPORTED_CODE);
assert.match(result.reason || '', /root\/sudo/i);
});

it('allows auto-approve for sandboxed root on linux', () => {
const result = getDangerouslySkipPermissionsSupport({
platform: 'linux',
uid: 0,
env: { IS_SANDBOX: '1' } as unknown as NodeJS.ProcessEnv,
});

assert.equal(result.supported, true);
});

it('allows auto-approve on windows', () => {
const result = getDangerouslySkipPermissionsSupport({
platform: 'win32',
uid: 0,
env: {} as NodeJS.ProcessEnv,
});

assert.equal(result.supported, true);
});
});
7 changes: 7 additions & 0 deletions src/components/layout/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ export function AppShell({ children }: { children: React.ReactNode }) {
}, []);
/* eslint-enable react-hooks/set-state-in-effect */

// Listen for mobile close events
useEffect(() => {
const handler = () => setChatListOpenRaw(false);
window.addEventListener("chatlist-close", handler);
return () => window.removeEventListener("chatlist-close", handler);
}, []);

// Panel width state with localStorage persistence
const [chatListWidth, setChatListWidth] = useState(240);

Expand Down
44 changes: 35 additions & 9 deletions src/components/layout/ChatListPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import {
MagnifyingGlass,
FileArrowDown,
Plus,
FolderOpen,
FolderPlus,
Lightning,
Plug,
Terminal,
Image,
WifiHigh,
Gear,
X,
} from "@/components/ui/icon";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
Expand Down Expand Up @@ -431,14 +433,37 @@ export function ChatListPanel({ open, width, hasUpdate, readyToInstall }: ChatLi
];

return (
<aside
className="hidden h-full shrink-0 flex-col overflow-hidden bg-sidebar/80 backdrop-blur-xl lg:flex"
style={{ width: width ?? 240 }}
>
{/* Header - extra top padding for macOS traffic lights */}
<div className="flex h-12 shrink-0 items-center justify-between px-3 mt-5">
<ConnectionStatus />
</div>
<>
{/* Mobile backdrop */}
<div
className="fixed inset-0 z-40 bg-black/50 lg:hidden"
onClick={() => {
window.dispatchEvent(new CustomEvent("chatlist-close"));
}}
/>

<aside
className="flex h-full shrink-0 flex-col overflow-hidden bg-sidebar/80 backdrop-blur-xl fixed inset-y-0 left-14 z-50 shadow-xl lg:static lg:left-0 lg:shadow-none"
style={{ width: width ?? 240 }}
>
{/* Mobile header with close button */}
<div className="flex lg:hidden h-12 shrink-0 items-center justify-between px-3 mt-5">
<ConnectionStatus />
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => window.dispatchEvent(new CustomEvent("chatlist-close"))}
aria-label="Close sidebar"
>
<X size={16} />
</Button>
</div>

{/* Desktop header */}
<div className="hidden lg:flex h-12 shrink-0 items-center justify-between px-3 mt-5">
<ConnectionStatus />
</div>

{/* Top action bar: New Chat + Search */}
<div className="flex items-center gap-2 px-3 pb-2">
Expand Down Expand Up @@ -728,6 +753,7 @@ export function ChatListPanel({ open, width, hasUpdate, readyToInstall }: ChatLi
onSelect={handleFolderSelect}
/>

</aside>
</aside>
</>
);
}
2 changes: 2 additions & 0 deletions src/hooks/useSSEStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface ToolUseInfo {
interface ToolResultInfo {
tool_use_id: string;
content: string;
is_error?: boolean;
media?: MediaBlock[];
}

Expand Down Expand Up @@ -74,6 +75,7 @@ function handleSSEEvent(
callbacks.onToolResult({
tool_use_id: resultData.tool_use_id,
content: resultData.content,
is_error: resultData.is_error,
...(Array.isArray(resultData.media) && resultData.media.length > 0
? { media: resultData.media }
: {}),
Expand Down
10 changes: 9 additions & 1 deletion src/lib/bridge/permission-broker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk';
import type { ChannelAddress, OutboundMessage } from './types';
import type { BaseChannelAdapter } from './channel-adapter';
import { deliver } from './delivery-layer';
import { insertPermissionLink, getPermissionLink, markPermissionLinkResolved, getSession, getDb } from '../db';
import { insertPermissionLink, getPermissionLink, markPermissionLinkResolved, getSession, getDb, getSetting } from '../db';
import { resolvePendingPermission } from '../permission-registry';
import { escapeHtml } from './adapters/telegram-utils';

Expand All @@ -36,6 +36,14 @@ export async function forwardPermissionRequest(
suggestions?: unknown[],
replyToMessageId?: string,
): Promise<void> {
// Check if auto-approval is enabled globally — auto-approve without IM notification
const globalAutoApprove = getSetting('dangerously_skip_permissions') === 'true';
if (globalAutoApprove) {
console.log(`[bridge] Auto-approved permission ${permissionRequestId} (tool=${toolName}) due to global auto-approval setting`);
resolvePendingPermission(permissionRequestId, { behavior: 'allow' });
return;
}

// Check if this session uses full_access permission profile — auto-approve without IM notification
if (sessionId) {
const session = getSession(sessionId);
Expand Down
Loading