mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 11:45:20 +02:00
feat: file drop relay + $B inbox command
Sidebar agent now writes structured messages to .context/sidebar-inbox/
when processing user input. The workspace agent can read these via
$B inbox to see what the user reported from the browser.
File drop format:
.context/sidebar-inbox/{timestamp}-observation.json
{ type, timestamp, page: {url}, userMessage, sidebarSessionId }
Atomic writes (tmp + rename) prevent partial reads. $B inbox --clear
removes messages after display.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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<string, { category: string; descriptio
|
||||
'connect': { category: 'Server', description: 'Launch headed Chromium with Chrome extension', usage: 'connect' },
|
||||
'disconnect': { category: 'Server', description: 'Disconnect headed browser, return to headless mode' },
|
||||
'focus': { category: 'Server', description: 'Bring headed browser window to foreground (macOS)', usage: 'focus [@ref]' },
|
||||
// Inbox
|
||||
'inbox': { category: 'Meta', description: 'List messages from sidebar scout inbox', usage: 'inbox [--clear]' },
|
||||
};
|
||||
|
||||
// Load-time validation: descriptions must cover exactly the command sets
|
||||
|
||||
@@ -327,6 +327,66 @@ export async function handleMetaCommand(
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Inbox ──────────────────────────────────────────
|
||||
case 'inbox': {
|
||||
const { execSync } = await import('child_process');
|
||||
let gitRoot: string;
|
||||
try {
|
||||
gitRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
||||
} catch {
|
||||
return 'Not in a git repository — cannot locate inbox.';
|
||||
}
|
||||
|
||||
const inboxDir = path.join(gitRoot, '.context', 'sidebar-inbox');
|
||||
if (!fs.existsSync(inboxDir)) return 'Inbox empty.';
|
||||
|
||||
const files = fs.readdirSync(inboxDir)
|
||||
.filter(f => 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}`);
|
||||
}
|
||||
|
||||
@@ -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<string | null> {
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user