mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-21 20:28:24 +08: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 |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `chain` | Run commands from JSON stdin. Format: [["cmd","arg1",...],...] |
|
| `chain` | Run commands from JSON stdin. Format: [["cmd","arg1",...],...] |
|
||||||
|
| `inbox [--clear]` | List messages from sidebar scout inbox |
|
||||||
|
|
||||||
### Tabs
|
### Tabs
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|
|||||||
@@ -585,6 +585,7 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`.
|
|||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `chain` | Run commands from JSON stdin. Format: [["cmd","arg1",...],...] |
|
| `chain` | Run commands from JSON stdin. Format: [["cmd","arg1",...],...] |
|
||||||
|
| `inbox [--clear]` | List messages from sidebar scout inbox |
|
||||||
|
|
||||||
### Tabs
|
### Tabs
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|
|||||||
1
SKILL.md
1
SKILL.md
@@ -591,6 +591,7 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`.
|
|||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `chain` | Run commands from JSON stdin. Format: [["cmd","arg1",...],...] |
|
| `chain` | Run commands from JSON stdin. Format: [["cmd","arg1",...],...] |
|
||||||
|
| `inbox [--clear]` | List messages from sidebar scout inbox |
|
||||||
|
|
||||||
### Tabs
|
### Tabs
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|
|||||||
@@ -463,6 +463,7 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`.
|
|||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `chain` | Run commands from JSON stdin. Format: [["cmd","arg1",...],...] |
|
| `chain` | Run commands from JSON stdin. Format: [["cmd","arg1",...],...] |
|
||||||
|
| `inbox [--clear]` | List messages from sidebar scout inbox |
|
||||||
|
|
||||||
### Tabs
|
### Tabs
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const META_COMMANDS = new Set([
|
|||||||
'url', 'snapshot',
|
'url', 'snapshot',
|
||||||
'handoff', 'resume',
|
'handoff', 'resume',
|
||||||
'connect', 'disconnect', 'focus',
|
'connect', 'disconnect', 'focus',
|
||||||
|
'inbox',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const ALL_COMMANDS = new Set([...READ_COMMANDS, ...WRITE_COMMANDS, ...META_COMMANDS]);
|
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' },
|
'connect': { category: 'Server', description: 'Launch headed Chromium with Chrome extension', usage: 'connect' },
|
||||||
'disconnect': { category: 'Server', description: 'Disconnect headed browser, return to headless mode' },
|
'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]' },
|
'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
|
// 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:
|
default:
|
||||||
throw new Error(`Unknown meta command: ${command}`);
|
throw new Error(`Unknown meta command: ${command}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,46 @@ let lastLine = 0;
|
|||||||
let authToken: string | null = null;
|
let authToken: string | null = null;
|
||||||
let isProcessing = false;
|
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 ────────────────────────────────────────────────────────
|
// ─── Auth ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function refreshToken(): Promise<string | null> {
|
async function refreshToken(): Promise<string | null> {
|
||||||
@@ -203,6 +243,8 @@ async function poll() {
|
|||||||
if (!entry.message && !entry.prompt) continue;
|
if (!entry.message && !entry.prompt) continue;
|
||||||
|
|
||||||
console.log(`[sidebar-agent] Processing: "${entry.message}"`);
|
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 {
|
try {
|
||||||
await askClaude(entry);
|
await askClaude(entry);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user