Skip to content
Draft
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
240 changes: 240 additions & 0 deletions apps/desktop/src/lib/trpc/routers/autocomplete/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import fs from "node:fs/promises";
import path from "node:path";
import { commandHistoryManager } from "main/lib/command-history";
import { z } from "zod";
import { publicProcedure, router } from "../..";

/**
* Autocomplete router for command history and file completion
*
* Provides:
* - Command history search with fuzzy matching
* - Recent command matching for ghost text suggestions
* - File/directory listing for path completion
*/
export const createAutocompleteRouter = () => {
return router({
/**
* Search command history with fuzzy matching
* Uses FTS5 for efficient search across all recorded commands
*/
searchHistory: publicProcedure
.input(
z.object({
query: z.string(),
limit: z.number().default(50),
workspaceId: z.string().optional(),
}),
)
.query(({ input }) => {
return commandHistoryManager.search({
query: input.query,
limit: input.limit,
workspaceId: input.workspaceId,
});
}),

/**
* Get the most recent command matching a prefix
* Used for inline ghost text suggestions (Fish-style autosuggestions)
*/
getRecentMatch: publicProcedure
.input(
z.object({
prefix: z.string(),
workspaceId: z.string().optional(),
}),
)
.query(({ input }) => {
return commandHistoryManager.getRecentMatch({
prefix: input.prefix,
workspaceId: input.workspaceId,
});
}),

/**
* Record a command execution
* Called when shell emits OSC 133 sequences
*/
recordCommand: publicProcedure
.input(
z.object({
command: z.string(),
workspaceId: z.string().optional(),
cwd: z.string().optional(),
exitCode: z.number().optional(),
}),
)
.mutation(({ input }) => {
commandHistoryManager.record({
command: input.command,
workspaceId: input.workspaceId,
cwd: input.cwd,
exitCode: input.exitCode,
});
}),

/**
* Get recent commands (no search query)
* Used for initial history picker display
*/
getRecent: publicProcedure
.input(
z.object({
limit: z.number().default(50),
workspaceId: z.string().optional(),
}),
)
.query(({ input }) => {
return commandHistoryManager.getRecent({
limit: input.limit,
workspaceId: input.workspaceId,
});
}),

/**
* List files and directories for path completion
* Supports partial path matching (e.g., "src/comp" matches "src/components/")
*/
listCompletions: publicProcedure
.input(
z.object({
partial: z.string(),
cwd: z.string(),
showHidden: z.boolean().default(false),
type: z.enum(["all", "files", "directories"]).default("all"),
}),
)
.query(async ({ input }) => {
const { partial, cwd, showHidden, type } = input;

try {
// Resolve the partial path
const isAbsolute = path.isAbsolute(partial);
const basePath = isAbsolute ? partial : path.join(cwd, partial);

// Check if partial ends with separator (user is in directory)
const _endsWithSep =
partial.endsWith("/") || partial.endsWith(path.sep);

// Get directory and prefix for filtering
let dirPath: string;
let prefix: string;

try {
const stat = await fs.stat(basePath);
if (stat.isDirectory()) {
dirPath = basePath;
prefix = "";
} else {
dirPath = path.dirname(basePath);
prefix = path.basename(basePath);
}
} catch {
// Path doesn't exist, treat as partial
dirPath = path.dirname(basePath);
prefix = path.basename(basePath);
}

// Read directory
const entries = await fs.readdir(dirPath, { withFileTypes: true });

// Filter and map entries
const completions = entries
.filter((entry) => {
// Filter by prefix
if (
prefix &&
!entry.name.toLowerCase().startsWith(prefix.toLowerCase())
) {
return false;
}

// Filter hidden files
if (!showHidden && entry.name.startsWith(".")) {
return false;
}

// Filter by type
if (type === "files" && entry.isDirectory()) {
return false;
}
if (type === "directories" && !entry.isDirectory()) {
return false;
}

return true;
})
.map((entry) => {
const isDirectory = entry.isDirectory();
const name = entry.name;
const fullPath = path.join(dirPath, name);

// Build the completion text (what to insert)
// If user typed "src/comp", completion for "components" should be "onents/"
const completionSuffix = isDirectory ? "/" : "";
const insertText = name.slice(prefix.length) + completionSuffix;

return {
name,
insertText,
fullPath,
isDirectory,
icon: isDirectory ? "folder" : getFileIcon(name),
};
})
.sort((a, b) => {
// Directories first, then alphabetical
if (a.isDirectory && !b.isDirectory) return -1;
if (!a.isDirectory && b.isDirectory) return 1;
return a.name.localeCompare(b.name);
})
.slice(0, 50); // Limit results

return {
basePath: dirPath,
prefix,
completions,
};
} catch (error) {
return {
basePath: cwd,
prefix: partial,
completions: [],
error: error instanceof Error ? error.message : "Unknown error",
};
}
}),
});
};

/**
* Get a simple icon identifier based on file extension
*/
function getFileIcon(filename: string): string {
const ext = path.extname(filename).toLowerCase();
const iconMap: Record<string, string> = {
".ts": "typescript",
".tsx": "react",
".js": "javascript",
".jsx": "react",
".json": "json",
".md": "markdown",
".css": "css",
".scss": "css",
".html": "html",
".py": "python",
".rs": "rust",
".go": "go",
".sh": "shell",
".bash": "shell",
".zsh": "shell",
".yml": "yaml",
".yaml": "yaml",
".toml": "config",
".env": "config",
".gitignore": "git",
".git": "git",
};
return iconMap[ext] || "file";
}
1 change: 1 addition & 0 deletions apps/desktop/src/lib/trpc/routers/autocomplete/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { createAutocompleteRouter } from "./autocomplete";
2 changes: 2 additions & 0 deletions apps/desktop/src/lib/trpc/routers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { router } from "..";
import { createAnalyticsRouter } from "./analytics";
import { createAuthRouter } from "./auth";
import { createAutoUpdateRouter } from "./auto-update";
import { createAutocompleteRouter } from "./autocomplete";
import { createChangesRouter } from "./changes";
import { createConfigRouter } from "./config";
import { createExternalRouter } from "./external";
Expand All @@ -29,6 +30,7 @@ export const createAppRouter = (getWindow: () => BrowserWindow | null) => {
return router({
analytics: createAnalyticsRouter(),
auth: createAuthRouter(getWindow),
autocomplete: createAutocompleteRouter(),
autoUpdate: createAutoUpdateRouter(),
user: createUserRouter(),
window: createWindowRouter(getWindow),
Expand Down
44 changes: 44 additions & 0 deletions apps/desktop/src/main/lib/agent-setup/shell-wrappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@ _superset_home="\${SUPERSET_ORIG_ZDOTDIR:-$HOME}"
export ZDOTDIR="$_superset_home"
[[ -f "$_superset_home/.zshrc" ]] && source "$_superset_home/.zshrc"
export PATH="$HOME/${SUPERSET_DIR_NAME}/bin:$PATH"

# Superset command history hooks
# OSC 133 sequences for shell integration
# C = command start (with command text), D = command done (with exit code)
_superset_preexec() {
# Emit OSC 133;C with the command being executed
printf '\\033]133;C;%s\\033\\\\' "\${1//[[:cntrl:]]}"
}
_superset_precmd() {
local exit_code=$?
# Emit OSC 133;D with the exit code
printf '\\033]133;D;%d\\033\\\\' "$exit_code"
}
# Add hooks if not already added
if [[ -z "\${_superset_hooks_installed}" ]]; then
autoload -Uz add-zsh-hook
add-zsh-hook preexec _superset_preexec
add-zsh-hook precmd _superset_precmd
_superset_hooks_installed=1
fi
`;
fs.writeFileSync(zshrcPath, zshrcScript, { mode: 0o644 });
console.log("[agent-setup] Created zsh wrapper");
Expand Down Expand Up @@ -58,6 +78,30 @@ fi
export PATH="$HOME/${SUPERSET_DIR_NAME}/bin:$PATH"
# Minimal prompt (path/env shown in toolbar) - emerald to match app theme
export PS1=$'\\[\\e[1;38;2;52;211;153m\\]❯\\[\\e[0m\\] '

# Superset command history hooks
# OSC 133 sequences for shell integration
_superset_last_cmd=""
_superset_trap_debug() {
# Capture the command before execution
_superset_last_cmd="$BASH_COMMAND"
}
_superset_prompt_command() {
local exit_code=$?
# Emit command done with exit code
printf '\\033]133;D;%d\\033\\\\' "$exit_code"
# Emit command start when we have a captured command
if [[ -n "$_superset_last_cmd" && "$_superset_last_cmd" != "_superset_prompt_command" ]]; then
printf '\\033]133;C;%s\\033\\\\' "\${_superset_last_cmd//[[:cntrl:]]}"
fi
_superset_last_cmd=""
}
Comment on lines +84 to +98
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: OSC 133 sequence order is incorrect for bash.

The bash implementation emits the C (command start) sequence in PROMPT_COMMAND (line 95), which runs after the command completes. According to OSC 133 semantics, C should be emitted before the command executes, and D should be emitted after.

The correct flow should be:

  1. DEBUG trap fires (before execution) → emit C with command
  2. Command executes
  3. PROMPT_COMMAND fires (after execution) → emit D with exit code

Currently, both C and D are emitted in PROMPT_COMMAND after the command completes, which will produce incorrect command tracking timestamps and ordering.

🔎 Proposed fix
 _superset_last_cmd=""
 _superset_trap_debug() {
   # Capture the command before execution
   _superset_last_cmd="$BASH_COMMAND"
+  # Emit command start when we have a command
+  if [[ -n "$_superset_last_cmd" && "$_superset_last_cmd" != "_superset_prompt_command" ]]; then
+    printf '\033]133;C;%s\033\\' "${_superset_last_cmd//[[:cntrl:]]}"
+  fi
 }
 _superset_prompt_command() {
   local exit_code=$?
   # Emit command done with exit code
   printf '\033]133;D;%d\033\\' "$exit_code"
-  # Emit command start when we have a captured command
-  if [[ -n "$_superset_last_cmd" && "$_superset_last_cmd" != "_superset_prompt_command" ]]; then
-    printf '\033]133;C;%s\033\\' "${_superset_last_cmd//[[:cntrl:]]}"
-  fi
   _superset_last_cmd=""
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_superset_last_cmd=""
_superset_trap_debug() {
# Capture the command before execution
_superset_last_cmd="$BASH_COMMAND"
}
_superset_prompt_command() {
local exit_code=$?
# Emit command done with exit code
printf '\\033]133;D;%d\\033\\\\' "$exit_code"
# Emit command start when we have a captured command
if [[ -n "$_superset_last_cmd" && "$_superset_last_cmd" != "_superset_prompt_command" ]]; then
printf '\\033]133;C;%s\\033\\\\' "\${_superset_last_cmd//[[:cntrl:]]}"
fi
_superset_last_cmd=""
}
_superset_last_cmd=""
_superset_trap_debug() {
# Capture the command before execution
_superset_last_cmd="$BASH_COMMAND"
# Emit command start when we have a command
if [[ -n "$_superset_last_cmd" && "$_superset_last_cmd" != "_superset_prompt_command" ]]; then
printf '\\033]133;C;%s\\033\\\\' "\${_superset_last_cmd//[[:cntrl:]]}"
fi
}
_superset_prompt_command() {
local exit_code=$?
# Emit command done with exit code
printf '\\033]133;D;%d\\033\\\\' "$exit_code"
_superset_last_cmd=""
}
🤖 Prompt for AI Agents
In apps/desktop/src/main/lib/agent-setup/shell-wrappers.ts around lines 84 to
98, the OSC 133 sequences are emitted in the wrong order because C (command
start) is produced in PROMPT_COMMAND which runs after the command; change the
logic so the DEBUG trap captures and immediately emits the C sequence with the
sanitized command before the command runs, and modify PROMPT_COMMAND to only
emit the D sequence with the exit code after the command completes; ensure the
DEBUG handler skips emitting when the command is the prompt handler itself,
properly escapes control chars in the emitted command, and clears any stored
last-command state in PROMPT_COMMAND after emitting D.

# Install hooks if not already installed
if [[ -z "\${_superset_hooks_installed}" ]]; then
trap '_superset_trap_debug' DEBUG
PROMPT_COMMAND="_superset_prompt_command\${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
_superset_hooks_installed=1
fi
`;
fs.writeFileSync(rcfilePath, script, { mode: 0o644 });
console.log("[agent-setup] Created bash wrapper");
Expand Down
Loading