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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "incognide",
"version": "0.1.4",
"version": "0.1.5",
"description": "Explore the unknown, build the future, own your data.",
"author": "Chris Agostino <info@npcworldwi.de>",
"main": "src/main.js",
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ flask_sse
redis
pyyaml
pillow
npcpy==1.3.25
npcpy==1.3.27
npcsh==1.1.23
anthropic
google-genai
Expand Down
3 changes: 2 additions & 1 deletion src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -1466,7 +1466,8 @@ body.light-mode .p-2.rounded-full.text-red-400 {
display: none;
}

body.layout-resizing .webview-resize-overlay {
body.layout-resizing .webview-resize-overlay,
body.layout-dragging .webview-resize-overlay {
display: block;
pointer-events: auto;
cursor: inherit;
Expand Down
18 changes: 15 additions & 3 deletions src/ipc/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,18 +350,30 @@ function register(ctx) {
wc.setWindowOpenHandler(({ url, disposition }) => {
log('[Browser] window.open intercepted:', url, 'disposition:', disposition);

// Google auth - open in system browser for proper auth flow
// Google auth — allow as popup so OAuth tokens stay in the webview's session
// (opening in system browser breaks gcloud login, Google Drive, Colab auth flows)
if (url.includes('accounts.google.com') ||
url.includes('accounts.youtube.com') ||
url.includes('myaccount.google.com')) {
shell.openExternal(url);
log('[Browser] Allowing Google auth popup in-app for OAuth flow');
return { action: 'allow' };
}

// Google Colab — open as new tab in the app
if (url.includes('colab.research.google.com')) {
log('[Browser] Opening Colab in new tab');
const mw = getMainWindow();
if (mw && !mw.isDestroyed()) {
mw.webContents.send('browser-open-in-new-tab', { url, disposition });
}
return { action: 'deny' };
}

// Google widgets (contacts hovercard, etc.) - allow as popups
if (url.includes('contacts.google.com/widget') ||
url.includes('apis.google.com') ||
url.includes('plus.google.com')) {
url.includes('plus.google.com') ||
url.includes('drive.google.com')) {
return { action: 'allow' };
}

Expand Down
10 changes: 10 additions & 0 deletions src/ipc/filesystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -1198,6 +1198,16 @@ function register(ctx) {
}
});

ipcMain.handle('copy-file', async (_, srcPath, destPath) => {
try {
await fsPromises.copyFile(srcPath, destPath);
return { success: true, error: null };
} catch (err) {
console.error('Error copying file:', err);
return { success: false, error: err.message };
}
});

// ============================================
// File Permission Management (chmod/chown)
// ============================================
Expand Down
58 changes: 26 additions & 32 deletions src/ipc/git.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,50 +10,44 @@ function register(ctx) {
const git = simpleGit(repoPath);
const status = await git.status();

const allChangedFiles = status.files.map(f => {
let fileStatus = '';
let isStaged = false;
let isUntracked = false;
const stagedFiles = [];
const unstagedFiles = [];
const untrackedFiles = [];

// Determine the primary status for display
for (const f of status.files) {
// A file can be both staged AND have unstaged changes (e.g. "MM")
// Handle staged status (index column)
if (f.index === 'M') {
fileStatus = 'Staged Modified'; // Modified in index
isStaged = true;
stagedFiles.push({ path: f.path, status: 'Modified', statusCode: 'M', isStaged: true, isUntracked: false });
} else if (f.index === 'A') {
fileStatus = 'Staged Added'; // Added to index
isStaged = true;
stagedFiles.push({ path: f.path, status: 'Added', statusCode: 'A', isStaged: true, isUntracked: false });
} else if (f.index === 'D') {
fileStatus = 'Staged Deleted'; // Deleted from index
isStaged = true;
} else if (f.working_dir === 'M') {
fileStatus = 'Modified'; // Modified in working directory, not staged
} else if (f.working_dir === 'D') {
fileStatus = 'Deleted'; // Deleted in working directory, not staged
} else if (f.index === '??') {
fileStatus = 'Untracked'; // Untracked file
isUntracked = true;
} else {
fileStatus = 'Unknown Change'; // Fallback for any other types
stagedFiles.push({ path: f.path, status: 'Deleted', statusCode: 'D', isStaged: true, isUntracked: false });
} else if (f.index === 'R') {
stagedFiles.push({ path: f.path, status: 'Renamed', statusCode: 'R', isStaged: true, isUntracked: false });
} else if (f.index === 'C') {
stagedFiles.push({ path: f.path, status: 'Copied', statusCode: 'C', isStaged: true, isUntracked: false });
}

return {
path: f.path,
status: fileStatus,
isStaged: isStaged,
isUntracked: isUntracked,
};
});
// Handle unstaged status (working_dir column)
if (f.working_dir === 'M') {
unstagedFiles.push({ path: f.path, status: 'Modified', statusCode: 'M', isStaged: false, isUntracked: false });
} else if (f.working_dir === 'D') {
unstagedFiles.push({ path: f.path, status: 'Deleted', statusCode: 'D', isStaged: false, isUntracked: false });
} else if (f.index === '?' || f.working_dir === '?') {
untrackedFiles.push({ path: f.path, status: 'Untracked', statusCode: '?', isStaged: false, isUntracked: true });
}
}

return {
success: true,
branch: status.current,
ahead: status.ahead,
behind: status.behind,
// Filter based on the new structured 'allChangedFiles'
staged: allChangedFiles.filter(f => f.isStaged),
unstaged: allChangedFiles.filter(f => !f.isStaged && !f.isUntracked),
untracked: allChangedFiles.filter(f => f.isUntracked),
hasChanges: allChangedFiles.length > 0
staged: stagedFiles,
unstaged: unstagedFiles,
untracked: untrackedFiles,
hasChanges: stagedFiles.length + unstagedFiles.length + untrackedFiles.length > 0
};
} catch (err) {
console.error(`[Git] Error getting status for ${repoPath}:`, err);
Expand Down
38 changes: 32 additions & 6 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -1386,11 +1386,17 @@ if (!gotTheLock) {
// Queue for URLs received before the main window is ready
let pendingDeepLinkUrl = null;

// Track last active window so URLs route to the right place
let lastActiveWindow = null;
app.on('browser-window-focus', (_, window) => { lastActiveWindow = window; });

// Helper to open a URL in the app's browser pane
const openUrlInBrowserPane = (targetUrl) => {
const windows = BrowserWindow.getAllWindows();
if (windows.length) {
const mainWindow = BrowserWindow.getFocusedWindow() || windows[0];
const mainWindow = BrowserWindow.getFocusedWindow() ||
(lastActiveWindow && !lastActiveWindow.isDestroyed() ? lastActiveWindow : null) ||
windows[0];
log(`[DEEP-LINK] Opening URL in browser pane: ${targetUrl}`);
mainWindow.webContents.send('open-url-in-browser', { url: targetUrl });
if (mainWindow.isMinimized()) mainWindow.restore();
Expand Down Expand Up @@ -1494,8 +1500,10 @@ if (!gotTheLock) {
app.on('second-instance', (event, commandLine, workingDirectory) => {
const windows = BrowserWindow.getAllWindows();
if (windows.length) {
// Use focused window if available, otherwise fall back to first window
const mainWindow = BrowserWindow.getFocusedWindow() || windows[0];
// Use focused window, then last active, then first window
const mainWindow = BrowserWindow.getFocusedWindow() ||
(lastActiveWindow && !lastActiveWindow.isDestroyed() ? lastActiveWindow : null) ||
windows[0];

// Parse CLI args from second instance
const folderArg = commandLine.find(arg => arg.startsWith('--folder='));
Expand Down Expand Up @@ -1836,9 +1844,27 @@ function createWindow(cliArgs = {}) {
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{
label: 'Actual Size',
accelerator: 'CmdOrCtrl+0',
click: (_, focusedWindow) => {
if (focusedWindow) focusedWindow.webContents.send('zoom-reset');
}
},
{
label: 'Zoom In',
accelerator: 'CmdOrCtrl+=',
click: (_, focusedWindow) => {
if (focusedWindow) focusedWindow.webContents.send('zoom-in');
}
},
{
label: 'Zoom Out',
accelerator: 'CmdOrCtrl+-',
click: (_, focusedWindow) => {
if (focusedWindow) focusedWindow.webContents.send('zoom-out');
}
},
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
Expand Down
61 changes: 31 additions & 30 deletions src/mcp_servers/incognide_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"""

import os
import sys
import json
import asyncio
import aiohttp
Expand Down Expand Up @@ -59,9 +60,9 @@ async def call_incognide_action(action: str, args: Dict[str, Any], window_id: st
Returns:
Result dictionary from the action
"""
import sys
effective_window_id = window_id or _target_window_id
print(f"[MCP SERVER] call_incognide_action: {action} window={effective_window_id}", file=sys.stderr)
# CRITICAL: Logging to stderr so it doesn't break MCP stdout JSON pipe
sys.stderr.write(f"[MCP SERVER] call_incognide_action: {action} window={effective_window_id}\n")
try:
payload = {"action": action, "args": args}
if effective_window_id:
Expand All @@ -77,13 +78,13 @@ async def call_incognide_action(action: str, args: Dict[str, Any], window_id: st
return result
else:
error_text = await response.text()
print(f"[MCP SERVER] Error: HTTP {response.status}: {error_text}", file=sys.stderr)
sys.stderr.write(f"[MCP SERVER] Error: HTTP {response.status}: {error_text}\n")
return {"success": False, "error": f"HTTP {response.status}: {error_text}"}
except aiohttp.ClientError as e:
print(f"[MCP SERVER] Connection error: {e}", file=sys.stderr)
sys.stderr.write(f"[MCP SERVER] Connection error: {e}\n")
return {"success": False, "error": f"Connection error: {str(e)}"}
except Exception as e:
print(f"[MCP SERVER] Exception: {e}", file=sys.stderr)
sys.stderr.write(f"[MCP SERVER] Exception: {e}\n")
import traceback
traceback.print_exc(file=sys.stderr)
return {"success": False, "error": f"Error: {str(e)}"}
Expand All @@ -102,7 +103,6 @@ async def list_windows() -> str:
Returns:
JSON array of windows with id, folder, title
"""
import sys
try:
async with aiohttp.ClientSession() as session:
async with session.get(
Expand All @@ -116,7 +116,7 @@ async def list_windows() -> str:
error_text = await response.text()
return json.dumps({"success": False, "error": f"HTTP {response.status}: {error_text}"})
except Exception as e:
print(f"[MCP SERVER] list_windows error: {e}", file=sys.stderr)
sys.stderr.write(f"[MCP SERVER] list_windows error: {e}\n")
return json.dumps({"success": False, "error": str(e)})


Expand Down Expand Up @@ -1233,26 +1233,27 @@ async def presentation_save(pane_id: str = "active") -> str:


if __name__ == "__main__":
print(f"Starting Incognide MCP server...")
print(f"Backend URL: {INCOGNIDE_BACKEND_URL}")
print(f"Available tools: list_windows, set_target_window, get_target_window,")
print(f" open_pane, close_pane, focus_pane, list_panes, list_pane_types,")
print(f" navigate_browser, show_diff, request_approval,")
print(f" notify, get_browser_info, split_pane, zen_mode, list_actions,")
print(f" run_terminal, browser_click, browser_type,")
print(f" get_browser_content, browser_screenshot, browser_eval,")
print(f" spreadsheet_read, spreadsheet_eval, spreadsheet_update_cell,")
print(f" spreadsheet_update_cells, spreadsheet_add_row, spreadsheet_delete_row,")
print(f" spreadsheet_add_column, spreadsheet_delete_column, spreadsheet_sort,")
print(f" spreadsheet_stats, spreadsheet_save, spreadsheet_export,")
print(f" spreadsheet_switch_sheet,")
print(f" document_read, document_eval, document_write,")
print(f" document_find_replace, document_format, document_insert_table,")
print(f" document_save, document_stats,")
print(f" presentation_read, presentation_read_slide, presentation_eval,")
print(f" presentation_update_text, presentation_add_slide,")
print(f" presentation_delete_slide, presentation_duplicate_slide,")
print(f" presentation_set_background, presentation_add_shape,")
print(f" presentation_save")

mcp.run(transport="stdio")
# CRITICAL: Using stderr for all debug messages so stdout remains pure JSON-RPC
sys.stderr.write(f"Starting Incognide MCP server...\n")
sys.stderr.write(f"Backend URL: {INCOGNIDE_BACKEND_URL}\n")
sys.stderr.write(f"Available tools: list_windows, set_target_window, get_target_window,\n")
sys.stderr.write(f" open_pane, close_pane, focus_pane, list_panes, list_pane_types,\n")
sys.stderr.write(f" navigate_browser, show_diff, request_approval,\n")
sys.stderr.write(f" notify, get_browser_info, split_pane, zen_mode, list_actions,\n")
sys.stderr.write(f" run_terminal, browser_click, browser_type,\n")
sys.stderr.write(f" get_browser_content, browser_screenshot, browser_eval,\n")
sys.stderr.write(f" spreadsheet_read, spreadsheet_eval, spreadsheet_update_cell,\n")
sys.stderr.write(f" spreadsheet_update_cells, spreadsheet_add_row, spreadsheet_delete_row,\n")
sys.stderr.write(f" spreadsheet_add_column, spreadsheet_delete_column, spreadsheet_sort,\n")
sys.stderr.write(f" spreadsheet_stats, spreadsheet_save, spreadsheet_export,\n")
sys.stderr.write(f" spreadsheet_switch_sheet,\n")
sys.stderr.write(f" document_read, document_eval, document_write,\n")
sys.stderr.write(f" document_find_replace, document_format, document_insert_table,\n")
sys.stderr.write(f" document_save, document_stats,\n")
sys.stderr.write(f" presentation_read, presentation_read_slide, presentation_eval,\n")
sys.stderr.write(f" presentation_update_text, presentation_add_slide,\n")
sys.stderr.write(f" presentation_delete_slide, presentation_duplicate_slide,\n")
sys.stderr.write(f" presentation_set_background, presentation_add_shape,\n")
sys.stderr.write(f" presentation_save\n")

mcp.run(transport="stdio")
18 changes: 18 additions & 0 deletions src/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ readDocxContent: (filePath) =>
readZipContents: (zipPath) => ipcRenderer.invoke('read-zip-contents', zipPath),
extractZip: (zipPath, targetDir, entryPath) => ipcRenderer.invoke('extract-zip', zipPath, targetDir, entryPath),
renameFile: (oldPath, newPath) => ipcRenderer.invoke('renameFile', oldPath, newPath),
copyFile: (srcPath, destPath) => ipcRenderer.invoke('copy-file', srcPath, destPath),
chmod: (options) => ipcRenderer.invoke('chmod', options),
chown: (options) => ipcRenderer.invoke('chown', options),

Expand Down Expand Up @@ -727,6 +728,23 @@ fileExists: (path) => ipcRenderer.invoke('file-exists', path),
return () => ipcRenderer.removeListener('jupyter:installProgress', handler);
},

// Zoom control events (from menu accelerators — forwarded to renderer so active webview can be zoomed)
onZoomIn: (callback) => {
const handler = () => callback();
ipcRenderer.on('zoom-in', handler);
return () => ipcRenderer.removeListener('zoom-in', handler);
},
onZoomOut: (callback) => {
const handler = () => callback();
ipcRenderer.on('zoom-out', handler);
return () => ipcRenderer.removeListener('zoom-out', handler);
},
onZoomReset: (callback) => {
const handler = () => callback();
ipcRenderer.on('zoom-reset', handler);
return () => ipcRenderer.removeListener('zoom-reset', handler);
},

// Version and Update APIs
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -582,10 +582,10 @@ const ChatInput: React.FC<ChatInputProps> = (props) => {
setIsHovering(false);

// Check for sidebar file drag
const jsonData = e.dataTransfer.getData('application/json');
if (jsonData) {
const sidebarData = e.dataTransfer.getData('application/x-sidebar-file') || e.dataTransfer.getData('application/json');
if (sidebarData) {
try {
const data = JSON.parse(jsonData);
const data = JSON.parse(sidebarData);
if (data.type === 'sidebar-file' && data.path) {
const fileName = getFileName(data.path) || data.path;
const existingNames = new Set(uploadedFiles.map((f: any) => f.name));
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/components/ContextFilesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ export const ContextFilesPanel: React.FC<ContextFilesPanelProps> = ({
}, []);

const handleDrop = useCallback((e: React.DragEvent) => {
const jsonData = e.dataTransfer.getData('application/json');
if (jsonData) {
const sidebarData = e.dataTransfer.getData('application/x-sidebar-file');
if (sidebarData) {
try {
const data = JSON.parse(jsonData);
const data = JSON.parse(sidebarData);
if (data.type === 'sidebar-file') {
return;
}
Expand Down
Loading
Loading