Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
0f846cd
track frontmost app on shortcut trigger
CREDO23 Mar 26, 2026
0abbfbf
save clipboard contents on shortcut trigger
CREDO23 Mar 26, 2026
6597649
add REPLACE_TEXT IPC channel
CREDO23 Mar 26, 2026
f931b4c
expose replaceText in preload
CREDO23 Mar 26, 2026
6e74f46
add replaceText type to ElectronAPI
CREDO23 Mar 26, 2026
5bb4f5c
implement replace handler with clipboard swap and paste-back
CREDO23 Mar 26, 2026
2adffcc
add paste-back button to assistant action bar
CREDO23 Mar 26, 2026
bc16c03
check accessibility permission before paste-back
CREDO23 Mar 26, 2026
2f08d40
destroy panel on dismiss, remove activate to preserve selection
CREDO23 Mar 26, 2026
1133a36
extract keyboard module with macOS and Windows support
CREDO23 Mar 27, 2026
f931f08
rename keyboard to platform module, add getSelectedText
CREDO23 Mar 27, 2026
2a8f393
add quick-ask action type definition
CREDO23 Mar 27, 2026
d48f6aa
add quick-ask page with default action menu
CREDO23 Mar 27, 2026
98e12dd
load /quick-ask page in panel
CREDO23 Mar 27, 2026
06f02fb
navigate directly to chat with search space id
CREDO23 Mar 27, 2026
6c59b3e
auto-submit quick-ask prompt from URL param
CREDO23 Mar 27, 2026
cc9cb39
show paste-back button only for transform actions
CREDO23 Mar 27, 2026
59e0579
simplify action menu to plain buttons, remove old quickAskText from t…
CREDO23 Mar 27, 2026
8d60fc7
remove searchSpacesAtom from quick-ask, forward params via dashboard
CREDO23 Mar 27, 2026
af2129e
move quick-ask page into dashboard route for auth context
CREDO23 Mar 27, 2026
f9a6e64
fix: don't clear pendingText on read to survive auth remount
CREDO23 Mar 27, 2026
151d6a8
use sessionStorage for quickAskMode to survive route changes
CREDO23 Mar 27, 2026
f36e5f8
redesign action menu: grid layout, search, Ask SurfSense, fix action …
CREDO23 Mar 27, 2026
9f13da3
fix Ask SurfSense: pre-fill with initialText and cursor positioning
CREDO23 Mar 27, 2026
58ac17f
fix: move quickAskMode to IPC to prevent sessionStorage leak between …
CREDO23 Mar 27, 2026
cb6f456
add / action trigger to InlineMentionEditor
CREDO23 Mar 28, 2026
3be0988
add ActionPicker component for / command trigger
CREDO23 Mar 28, 2026
c2644aa
wire / action picker in Composer with keyboard navigation
CREDO23 Mar 28, 2026
407059c
add action chip in composer with prompt prepend at send time
CREDO23 Mar 28, 2026
041401a
add custom quick-ask actions: model, migration, schemas, CRUD routes
CREDO23 Mar 27, 2026
1137424
restore custom actions API service and wire to ActionPicker
CREDO23 Mar 28, 2026
a6ccb7a
rename quick-ask-actions to prompts across backend and frontend
CREDO23 Mar 28, 2026
03ca4f1
add My Prompts settings tab and create prompt button in picker
CREDO23 Mar 28, 2026
6df9eea
desktop: clipboard auto-insert with prompt picker, remove old session…
CREDO23 Mar 28, 2026
cfddfa5
fix: harden quick-ask panel, prompt handling, and clipboard UX
CREDO23 Mar 29, 2026
b8a1d1f
Merge remote-tracking branch 'upstream/dev' into electon-desktop
CREDO23 Mar 29, 2026
726fec7
fix: renumber prompts migration to 110 to follow upstream folders mig…
CREDO23 Mar 29, 2026
ab3c636
Merge remote-tracking branch 'upstream/dev' into electon-desktop
CREDO23 Mar 29, 2026
3363c71
fix: renumber prompts migration to 111 to follow upstream OneDrive mi…
CREDO23 Mar 29, 2026
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
50 changes: 50 additions & 0 deletions surfsense_backend/alembic/versions/111_add_prompts_table.py
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CREDO23 It should be no 111

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""add prompts table

Revision ID: 111
Revises: 110
"""

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

revision: str = "111"
down_revision: str | None = "110"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
conn = op.get_bind()

result = conn.execute(
sa.text("SELECT 1 FROM pg_type WHERE typname = 'prompt_mode'")
)
if not result.fetchone():
op.execute("CREATE TYPE prompt_mode AS ENUM ('transform', 'explore')")

result = conn.execute(
sa.text("SELECT 1 FROM information_schema.tables WHERE table_name = 'prompts'")
)
if not result.fetchone():
op.execute("""
CREATE TABLE prompts (
id SERIAL PRIMARY KEY,
user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
search_space_id INTEGER REFERENCES searchspaces(id) ON DELETE CASCADE,
name VARCHAR(200) NOT NULL,
prompt TEXT NOT NULL,
mode prompt_mode NOT NULL,
icon VARCHAR(50),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
)
""")
op.execute("CREATE INDEX ix_prompts_user_id ON prompts (user_id)")
op.execute("CREATE INDEX ix_prompts_search_space_id ON prompts (search_space_id)")


def downgrade() -> None:
op.execute("DROP TABLE IF EXISTS prompts")
op.execute("DROP TYPE IF EXISTS prompt_mode")
29 changes: 29 additions & 0 deletions surfsense_backend/app/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -1775,6 +1775,35 @@ class SearchSpaceInvite(BaseModel, TimestampMixin):
)


class PromptMode(StrEnum):
TRANSFORM = "transform"
EXPLORE = "explore"


class Prompt(BaseModel, TimestampMixin):
__tablename__ = "prompts"

user_id = Column(
UUID(as_uuid=True),
ForeignKey("user.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
search_space_id = Column(
Integer,
ForeignKey("searchspaces.id", ondelete="CASCADE"),
nullable=True,
index=True,
)
name = Column(String(200), nullable=False)
prompt = Column(Text, nullable=False)
mode = Column(SQLAlchemyEnum(PromptMode), nullable=False)
icon = Column(String(50), nullable=True)

user = relationship("User")
search_space = relationship("SearchSpace")


if config.AUTH_TYPE == "GOOGLE":

class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, Base):
Expand Down
2 changes: 2 additions & 0 deletions surfsense_backend/app/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from .notion_add_connector_route import router as notion_add_connector_router
from .podcasts_routes import router as podcasts_router
from .public_chat_routes import router as public_chat_router
from .prompts_routes import router as prompts_router
from .rbac_routes import router as rbac_router
from .reports_routes import router as reports_router
from .sandbox_routes import router as sandbox_router
Expand Down Expand Up @@ -89,3 +90,4 @@
router.include_router(public_chat_router) # Public chat sharing and cloning
router.include_router(incentive_tasks_router) # Incentive tasks for earning free pages
router.include_router(youtube_router) # YouTube playlist resolution
router.include_router(prompts_router)
94 changes: 94 additions & 0 deletions surfsense_backend/app/routes/prompts_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.db import Prompt, User, get_async_session
from app.schemas.prompts import (
PromptCreate,
PromptRead,
PromptUpdate,
)
from app.users import current_active_user

router = APIRouter(tags=["Prompts"])


@router.get("/prompts", response_model=list[PromptRead])
async def list_prompts(
search_space_id: int | None = None,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
):
query = select(Prompt).where(Prompt.user_id == user.id)
if search_space_id is not None:
query = query.where(Prompt.search_space_id == search_space_id)
query = query.order_by(Prompt.created_at.desc())
result = await session.execute(query)
return result.scalars().all()


@router.post("/prompts", response_model=PromptRead)
async def create_prompt(
body: PromptCreate,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
):
prompt = Prompt(
user_id=user.id,
search_space_id=body.search_space_id,
name=body.name,
prompt=body.prompt,
mode=body.mode,
icon=body.icon,
)
session.add(prompt)
await session.commit()
await session.refresh(prompt)
return prompt


@router.put("/prompts/{prompt_id}", response_model=PromptRead)
async def update_prompt(
prompt_id: int,
body: PromptUpdate,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
):
result = await session.execute(
select(Prompt).where(
Prompt.id == prompt_id,
Prompt.user_id == user.id,
)
)
prompt = result.scalar_one_or_none()
if not prompt:
raise HTTPException(status_code=404, detail="Prompt not found")

for field, value in body.model_dump(exclude_unset=True).items():
setattr(prompt, field, value)

session.add(prompt)
await session.commit()
await session.refresh(prompt)
return prompt


@router.delete("/prompts/{prompt_id}")
async def delete_prompt(
prompt_id: int,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
):
result = await session.execute(
select(Prompt).where(
Prompt.id == prompt_id,
Prompt.user_id == user.id,
)
)
prompt = result.scalar_one_or_none()
if not prompt:
raise HTTPException(status_code=404, detail="Prompt not found")

await session.delete(prompt)
await session.commit()
return {"success": True}
31 changes: 31 additions & 0 deletions surfsense_backend/app/schemas/prompts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from datetime import datetime

from pydantic import BaseModel, Field


class PromptCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=200)
prompt: str = Field(..., min_length=1)
mode: str = Field(..., pattern="^(transform|explore)$")
icon: str | None = Field(None, max_length=50)
search_space_id: int | None = None


class PromptUpdate(BaseModel):
name: str | None = Field(None, min_length=1, max_length=200)
prompt: str | None = Field(None, min_length=1)
mode: str | None = Field(None, pattern="^(transform|explore)$")
icon: str | None = Field(None, max_length=50)


class PromptRead(BaseModel):
id: int
name: str
prompt: str
mode: str
icon: str | None
search_space_id: int | None
created_at: datetime

class Config:
from_attributes = True
3 changes: 3 additions & 0 deletions surfsense_desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ export const IPC_CHANNELS = {
GET_APP_VERSION: 'get-app-version',
DEEP_LINK: 'deep-link',
QUICK_ASK_TEXT: 'quick-ask-text',
SET_QUICK_ASK_MODE: 'set-quick-ask-mode',
GET_QUICK_ASK_MODE: 'get-quick-ask-mode',
REPLACE_TEXT: 'replace-text',
} as const;
55 changes: 55 additions & 0 deletions surfsense_desktop/src/modules/platform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { execSync } from 'child_process';
import { systemPreferences } from 'electron';

export function getFrontmostApp(): string {
try {
if (process.platform === 'darwin') {
return execSync(
'osascript -e \'tell application "System Events" to get name of first application process whose frontmost is true\''
).toString().trim();
}
if (process.platform === 'win32') {
return execSync(
'powershell -command "Add-Type \'using System; using System.Runtime.InteropServices; public class W { [DllImport(\\\"user32.dll\\\")] public static extern IntPtr GetForegroundWindow(); }\'; (Get-Process | Where-Object { $_.MainWindowHandle -eq [W]::GetForegroundWindow() }).ProcessName"'
).toString().trim();
}
} catch {
return '';
}
return '';
}

export function getSelectedText(): string {
try {
if (process.platform === 'darwin') {
return execSync(
'osascript -e \'tell application "System Events" to get value of attribute "AXSelectedText" of focused UI element of first application process whose frontmost is true\''
).toString().trim();
}
// Windows: no reliable accessibility API for selected text across apps
} catch {
return '';
}
return '';
}

export function simulateCopy(): void {
if (process.platform === 'darwin') {
execSync('osascript -e \'tell application "System Events" to keystroke "c" using command down\'');
} else if (process.platform === 'win32') {
execSync('powershell -command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait(\'^c\')"');
}
}

export function simulatePaste(): void {
if (process.platform === 'darwin') {
execSync('osascript -e \'tell application "System Events" to keystroke "v" using command down\'');
} else if (process.platform === 'win32') {
execSync('powershell -command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait(\'^v\')"');
}
}

export function checkAccessibilityPermission(): boolean {
if (process.platform !== 'darwin') return true;
return systemPreferences.isTrustedAccessibilityClient(true);
}
Loading
Loading