diff --git a/.agents/skills/gstack-browse/SKILL.md b/.agents/skills/gstack-browse/SKILL.md index 19b2db98..69ddbd6f 100644 --- a/.agents/skills/gstack-browse/SKILL.md +++ b/.agents/skills/gstack-browse/SKILL.md @@ -457,6 +457,7 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`. | Command | Description | |---------|-------------| | `chain` | Run commands from JSON stdin. Format: [["cmd","arg1",...],...] | +| `inbox [--clear]` | List messages from sidebar scout inbox | ### Tabs | Command | Description | diff --git a/.agents/skills/gstack/SKILL.md b/.agents/skills/gstack/SKILL.md index 93429955..d450f81d 100644 --- a/.agents/skills/gstack/SKILL.md +++ b/.agents/skills/gstack/SKILL.md @@ -585,6 +585,7 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`. | Command | Description | |---------|-------------| | `chain` | Run commands from JSON stdin. Format: [["cmd","arg1",...],...] | +| `inbox [--clear]` | List messages from sidebar scout inbox | ### Tabs | Command | Description | diff --git a/SKILL.md b/SKILL.md index 46d5a7c6..b1c1a189 100644 --- a/SKILL.md +++ b/SKILL.md @@ -591,6 +591,7 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`. | Command | Description | |---------|-------------| | `chain` | Run commands from JSON stdin. Format: [["cmd","arg1",...],...] | +| `inbox [--clear]` | List messages from sidebar scout inbox | ### Tabs | Command | Description | diff --git a/browse/SKILL.md b/browse/SKILL.md index 582be8a7..d6a1236d 100644 --- a/browse/SKILL.md +++ b/browse/SKILL.md @@ -463,6 +463,7 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`. | Command | Description | |---------|-------------| | `chain` | Run commands from JSON stdin. Format: [["cmd","arg1",...],...] | +| `inbox [--clear]` | List messages from sidebar scout inbox | ### Tabs | Command | Description | diff --git a/browse/src/commands.ts b/browse/src/commands.ts index 5e3f9c45..2376c958 100644 --- a/browse/src/commands.ts +++ b/browse/src/commands.ts @@ -32,6 +32,7 @@ export const META_COMMANDS = new Set([ 'url', 'snapshot', 'handoff', 'resume', 'connect', 'disconnect', 'focus', + 'inbox', ]); export const ALL_COMMANDS = new Set([...READ_COMMANDS, ...WRITE_COMMANDS, ...META_COMMANDS]); @@ -103,6 +104,8 @@ export const COMMAND_DESCRIPTIONS: Record f.endsWith('.json') && !f.startsWith('.')) + .sort() + .reverse(); // newest first + + if (files.length === 0) return 'Inbox empty.'; + + const messages: { timestamp: string; url: string; userMessage: string }[] = []; + for (const file of files) { + try { + const data = JSON.parse(fs.readFileSync(path.join(inboxDir, file), 'utf-8')); + messages.push({ + timestamp: data.timestamp || '', + url: data.page?.url || 'unknown', + userMessage: data.userMessage || '', + }); + } catch { + // Skip malformed files + } + } + + if (messages.length === 0) return 'Inbox empty.'; + + const lines: string[] = []; + lines.push(`SIDEBAR INBOX (${messages.length} message${messages.length === 1 ? '' : 's'})`); + lines.push('────────────────────────────────'); + + for (const msg of messages) { + const ts = msg.timestamp ? `[${msg.timestamp}]` : '[unknown]'; + lines.push(`${ts} ${msg.url}`); + lines.push(` "${msg.userMessage}"`); + lines.push(''); + } + + lines.push('────────────────────────────────'); + + // Handle --clear flag + if (args.includes('--clear')) { + for (const file of files) { + try { fs.unlinkSync(path.join(inboxDir, file)); } catch {} + } + lines.push(`Cleared ${files.length} message${files.length === 1 ? '' : 's'}.`); + } + + return lines.join('\n'); + } + default: throw new Error(`Unknown meta command: ${command}`); } diff --git a/browse/src/sidebar-agent.ts b/browse/src/sidebar-agent.ts index 16628fb5..7972c7fb 100644 --- a/browse/src/sidebar-agent.ts +++ b/browse/src/sidebar-agent.ts @@ -23,6 +23,46 @@ let lastLine = 0; let authToken: string | null = null; let isProcessing = false; +// ─── File drop relay ────────────────────────────────────────── + +function getGitRoot(): string | null { + try { + const { execSync } = require('child_process'); + return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); + } catch { + return null; + } +} + +function writeToInbox(message: string, pageUrl?: string, sessionId?: string): void { + const gitRoot = getGitRoot(); + if (!gitRoot) { + console.error('[sidebar-agent] Cannot write to inbox — not in a git repo'); + return; + } + + const inboxDir = path.join(gitRoot, '.context', 'sidebar-inbox'); + fs.mkdirSync(inboxDir, { recursive: true }); + + const now = new Date(); + const timestamp = now.toISOString().replace(/:/g, '-'); + const filename = `${timestamp}-observation.json`; + const tmpFile = path.join(inboxDir, `.${filename}.tmp`); + const finalFile = path.join(inboxDir, filename); + + const inboxMessage = { + type: 'observation', + timestamp: now.toISOString(), + page: { url: pageUrl || 'unknown', title: '' }, + userMessage: message, + sidebarSessionId: sessionId || 'unknown', + }; + + fs.writeFileSync(tmpFile, JSON.stringify(inboxMessage, null, 2)); + fs.renameSync(tmpFile, finalFile); + console.log(`[sidebar-agent] Wrote inbox message: ${filename}`); +} + // ─── Auth ──────────────────────────────────────────────────────── async function refreshToken(): Promise { @@ -203,6 +243,8 @@ async function poll() { if (!entry.message && !entry.prompt) continue; console.log(`[sidebar-agent] Processing: "${entry.message}"`); + // Write to inbox so workspace agent can pick it up + writeToInbox(entry.message || entry.prompt, entry.pageUrl, entry.sessionId); try { await askClaude(entry); } catch (err) {