Skip to content
Closed
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
51 changes: 51 additions & 0 deletions backend/routes/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,57 @@ router.get('/:podId', auth, getMessages);
router.post('/:podId', sendMessageRateLimit, auth, createMessage);
router.delete('/:id', auth, deleteMessage);

// Backdate a message's `created_at`. Pod-creator-only (or message author);
// no general-purpose user authorization for editing other people's chronology.
//
// Why this exists: chat history demos / fixtures need plausible timestamps
// (yesterday / this morning / etc.) but POST /:podId always stamps with NOW().
// Direct PG UPDATE works for ops with PG access; this route is the same
// capability for ops without PG access (Aiven IP-allowlisted, kubectl exec
// timing out on new sessions, etc.).
//
// Body: { created_at: ISO8601 string }
router.patch('/:id/created-at', auth, async (req: any, res: any) => {
try {
const { id } = req.params;
const { created_at: createdAtRaw } = req.body || {};
const ts = createdAtRaw ? new Date(createdAtRaw) : null;
if (!ts || Number.isNaN(ts.getTime())) {
return res.status(400).json({ msg: 'created_at (ISO8601) is required' });
}

// Authorization: load the message, then check author or pod-creator.
// eslint-disable-next-line @typescript-eslint/no-require-imports, global-require
const PGMessage = require('../models/pg/Message');
// eslint-disable-next-line @typescript-eslint/no-require-imports, global-require
const Pod = require('../models/Pod');
const userId = req.userId || req.user?.id;
if (!userId) return res.status(401).json({ msg: 'auth required' });

const message = await PGMessage.findById(id);
if (!message) return res.status(404).json({ msg: 'message not found' });

if (String(message.user_id) !== String(userId)) {
const pod = await Pod.findById(message.pod_id);
if (!pod || String(pod.createdBy) !== String(userId)) {
return res.status(403).json({ msg: 'must be message author or pod creator' });
}
}

// eslint-disable-next-line @typescript-eslint/no-require-imports, global-require
const { pool } = require('../config/db-pg');
const r = await pool.query(
'UPDATE messages SET created_at = $1, updated_at = $1 WHERE id = $2 RETURNING id, created_at',
[ts, id],
);
if (r.rowCount === 0) return res.status(404).json({ msg: 'message not found in PG' });
return res.json({ ok: true, id: r.rows[0].id, created_at: r.rows[0].created_at });
} catch (err: any) {
console.error('backdate route error:', err?.message || err);
return res.status(500).json({ msg: 'backdate failed' });
}
});

module.exports = router;

export {};
52 changes: 30 additions & 22 deletions frontend/src/v2/components/V2PodChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -572,28 +572,36 @@ const V2PodChat: React.FC<V2PodChatProps> = ({ detail, inspectorCollapsed, onTog
</div>
)}

<div className={`v2-chat__mode-toggle v2-chat__mode-toggle--header v2-chat__mode-toggle--${mode}`} role="group" aria-label="Pod mode preference">
<button
type="button"
className={`v2-chat__mode-option${mode === 'plan' ? ' v2-chat__mode-option--active' : ''}`}
onClick={() => handleSetMode('plan')}
aria-pressed={mode === 'plan'}
title={modeCopy('plan')}
>
<Icon d="M9 11l3 3L22 4M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11" />
Plan
</button>
<button
type="button"
className={`v2-chat__mode-option${mode === 'execute' ? ' v2-chat__mode-option--active' : ''}`}
onClick={() => handleSetMode('execute')}
aria-pressed={mode === 'execute'}
title={modeCopy('execute')}
>
<Icon d="M5 3l14 9-14 9V3z" />
Execute
</button>
</div>
{/* Plan / Execute mode toggle — hidden until the pod-mode workflow
ships end-to-end. Currently the toggle persists `mode` to the
pod but no downstream surface uses it for behavior, so the
control reads as broken — clicks land but nothing changes for
the user. Re-enable when the mode actually drives behavior
(agent autonomy gating, suggestion ranking, etc.). */}
{false && (
<div className={`v2-chat__mode-toggle v2-chat__mode-toggle--header v2-chat__mode-toggle--${mode}`} role="group" aria-label="Pod mode preference">
<button
type="button"
className={`v2-chat__mode-option${mode === 'plan' ? ' v2-chat__mode-option--active' : ''}`}
onClick={() => handleSetMode('plan')}
aria-pressed={mode === 'plan'}
title={modeCopy('plan')}
>
<Icon d="M9 11l3 3L22 4M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11" />
Plan
</button>
<button
type="button"
className={`v2-chat__mode-option${mode === 'execute' ? ' v2-chat__mode-option--active' : ''}`}
onClick={() => handleSetMode('execute')}
aria-pressed={mode === 'execute'}
title={modeCopy('execute')}
>
<Icon d="M5 3l14 9-14 9V3z" />
Execute
</button>
</div>
)}

{onOpenInvite && (
<button
Expand Down
Loading