Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ src/vibes/static/common/fonts/vendor/** linguist-vendored=true

# Test and parity fixtures are not production implementation languages.
tests/**/*.mjs linguist-detectable=false
# Preserve the byte-identical canonical UX fixture on Windows checkouts.
tests/ux/features/canonical-ux.feature text eol=lf
# Bun emits meaningful multiline template literals; do not rewrite their whitespace.
src/vibes/static/dist/*.js -whitespace

# Documentation media.
docs/demo.gif linguist-documentation=true
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ Model and thinking controls depend on the selected adapter. Context occupancy an

Conversations have separate stored timelines and agent-session bindings, but they share a server account and workspace. They are not security sandboxes. ACP text attachments can be read through the optional scoped messages tool; image and binary attachments have metadata-only retrieval through that tool, not guaranteed native model input. See the [agent file-access contract][files] for the exact boundary.

See [drafts, timestamps and permission review](docs/BROWSER_REVIEW.md) for streaming behavior, UTC time labels, exact command copying and approval-dialog controls.

## Keep it private

Vibes binds to `127.0.0.1` by default and has **no built-in login**. Anyone who can reach its API can read or modify workspace files and operate the agent; enabling the terminal also gives them a shell as the server user. Put authentication and HTTPS in front of it before enabling remote access, including on a tailnet shared with other people.
Expand Down
25 changes: 25 additions & 0 deletions docs/BROWSER_REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Drafts, timestamps and permission review

The browser keeps streamed drafts separate from stored final messages. Drafts can change while tools run; only the persisted final response establishes completion.

## Draft streaming

Collapsed panels accept replacement preview snapshots and explicitly marked append chunks. Expanding a panel retrieves its full current text and consumes the dedicated delta stream. The browser does not consume both copies of a chunk. A missing or disconnected stream is not proof that the turn ended; session-scoped status polling restores known turn state.

## Message time

SQLite timestamps in `YYYY-MM-DD HH:MM:SS` form are UTC even though they omit a timezone suffix. The browser normalises that database format, preserves explicit ISO offsets, and displays relative ages. Labels refresh every 30 seconds. Hover over a message time for its absolute time in the browser's locale. These are server message timestamps; the browser clock still affects the displayed age.

## Reviewing a request

Permission requests show the proposed action, its description and available arguments. Native-shaped shell requests prioritise `fullCommandText`, show runtime warnings and working-directory/path hints, and keep the unmodified payload under **Technical details**. These hints come from the requesting backend and are not an independent safety verdict.

Commands wrap visually without changing or truncating their text. **Copy** copies the exact command, including line breaks; a browser selection fallback supports HTTP deployments without the Clipboard API. One vertically scrolling body keeps the action footer visible on narrow screens. The badge uses theme-aware contrast, and keyboard focus remains inside the dialog.

**Deny** and **Allow once** send the backend's original option IDs. Copying text, opening details, pressing Escape or clicking the backdrop does not approve a request. There is no implicit approval on opening the dialog. A timed-out, cancelled or already answered request may be rejected by the backend; the browser cannot revive it. Closing one request does not dismiss a newer request.

Some backends ask questions rather than permission. Choices and explicitly allowed free-form answers remain distinct from approval decisions. Browser support for a payload shape does not enable backend capabilities or install the Copilot SDK.

## Verification

Frontend unit tests cover timestamp parsing, append/snapshot handling, permission field extraction and SSE forwarding. Browser tests cover UTC/Lisbon/New York ages and refresh, expanded draft deduplication, exact copy, focus containment, and long commands at desktop/phone/landscape sizes. Speech and backend fixtures are synthetic; browser tests never approve a live request.
12 changes: 8 additions & 4 deletions src/vibes/message_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,14 @@ async def query(self, action, *, row_ids=None, query='', limit=10, before_row=No
authorized_requested = set()
for row_id in requested:
async with self.connection.execute('SELECT i.id FROM interactions i WHERE ' + where + ' AND i.id=?', [*scope_params, row_id]) as cursor:
if await cursor.fetchone(): authorized_requested.add(row_id)
if await cursor.fetchone():
authorized_requested.add(row_id)
if context_before or context_after:
selected = set(authorized_requested)
for row_id in authorized_requested:
for op, count, order in [('<', context_before, 'DESC'), ('>', context_after, 'ASC')]:
if not count: continue
if not count:
continue
async with self.connection.execute('SELECT i.id FROM interactions i WHERE ' + where + f' AND i.id {op} ? ORDER BY i.id {order} LIMIT ?', [*scope_params, row_id, count]) as cursor:
selected.update(row[0] for row in await cursor.fetchall())
clauses.append('i.id IN (' + ','.join('?' for _ in selected or [0]) + ')')
Expand All @@ -122,9 +124,11 @@ async def query(self, action, *, row_ids=None, query='', limit=10, before_row=No
else:
raise ValueError('Unsupported messages action')
if before_row is not None:
clauses.append('i.id < ?'); params.append(before_row)
clauses.append('i.id < ?')
params.append(before_row)
if after_row is not None:
clauses.append('i.id > ?'); params.append(after_row)
clauses.append('i.id > ?')
params.append(after_row)
sql = 'SELECT i.id, i.timestamp, i.data FROM interactions i WHERE ' + ' AND '.join(clauses) + ' ORDER BY i.id DESC LIMIT ?'
params.append(limit + 1)
async with self.connection.execute(sql, params) as cursor:
Expand Down
55 changes: 55 additions & 0 deletions src/vibes/static/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -469,3 +469,58 @@ button.compose-context-pie.icon-btn:disabled {
.quick-actions-close-hint { appearance: none; background: none; border: 0; padding: 0; color: inherit; font: inherit; cursor: pointer; }
.quick-actions-close-hint:focus-visible { outline: 2px solid var(--accent-color); outline-offset: 3px; }
.compose-session-trigger .compose-current-agent-label.active { color: #fff; }

/* Reviewable permissions: one scrolling body, full wrapped commands, pinned actions. */
.permission-review { box-sizing: border-box; background: rgb(0 0 0 / 55%); padding: 20px; }
.permission-review .agent-request-content {
width: 100%; max-width: 780px; min-width: 0; padding: 0; overflow: hidden;
max-height: min(880px, calc(var(--app-height, 100dvh) - 40px));
background: var(--bg-primary); color: var(--text-primary); border-color: var(--border-color);
box-shadow: 0 20px 70px rgb(0 0 0 / 30%); border-radius: 14px;
}
.permission-review .agent-request-header { flex: 0 0 auto; margin: 0; padding: 20px 24px; border-bottom: 1px solid var(--border-color); align-items: flex-start; }
.permission-review .agent-request-icon { background: var(--bg-secondary); color: var(--text-secondary); border: 1px solid var(--border-color); }
.permission-review .agent-request-icon svg { stroke: currentColor; color: inherit; fill: none; }
.agent-request-heading-group { min-width: 0; flex: 1; }
.agent-request-eyebrow { color: var(--text-secondary); font-size: 12px; margin-bottom: 4px; }
.permission-review .agent-request-title { margin: 0; color: var(--text-primary); font-size: 20px; line-height: 1.3; overflow-wrap: anywhere; }
.permission-review .agent-request-body { flex: 1 1 auto; min-height: 0; min-width: 0; margin: 0; padding: 20px 24px; overflow: auto; overscroll-behavior: contain; scrollbar-gutter: stable; }
.permission-review .agent-request-description { color: var(--text-secondary); white-space: pre-wrap; overflow-wrap: anywhere; margin-bottom: 16px; }
.permission-review .agent-request-files { min-width: 0; color: var(--text-secondary); }
.permission-review .agent-request-files li { overflow-wrap: anywhere; word-break: break-word; }
.permission-review .agent-request-subtitle { color: var(--text-secondary); text-transform: none; letter-spacing: normal; font-size: 12px; font-weight: 600; overflow-wrap: anywhere; }
.agent-request-command-section { margin: 0 0 18px; border: 1px solid var(--border-color); border-radius: 8px; overflow: hidden; background: var(--bg-secondary); }
.agent-request-section-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 6px 12px; border-bottom: 1px solid var(--border-color); }
.agent-request-copy { color: var(--text-primary); background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 6px; padding: 5px 10px; min-height: 32px; font: inherit; font-size: 12px; cursor: pointer; }
.permission-review .agent-request-command, .permission-review .agent-request-diff pre {
box-sizing: border-box; min-width: 0; max-width: 100%; max-height: none;
white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; overflow: visible;
color: var(--text-primary); background: var(--bg-secondary); font-family: var(--font-family-mono); font-size: 13px; line-height: 1.6;
padding: 12px; tab-size: 4; margin: 0;
}
.agent-request-command-note { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px; padding: 0 12px 10px; color: var(--text-secondary); font-size: 11px; }
.permission-review .agent-request-diff { color: var(--text-secondary); margin-top: 16px; }
.permission-review .agent-request-diff summary { color: var(--text-secondary); min-height: 28px; overflow-wrap: anywhere; }
.agent-request-footer { flex: 0 0 auto; padding: 16px 24px; border-top: 1px solid var(--border-color); background: var(--bg-primary); }
.agent-request-scope { color: var(--text-secondary); font-size: 12px; line-height: 1.4; margin: 0 0 12px; }
.permission-review .permission-actions { flex-direction: row; justify-content: flex-end; flex-wrap: wrap; gap: 10px; }
.permission-review .agent-request-btn { color: var(--text-primary); background: var(--bg-secondary); border-color: var(--border-color); padding: 10px 18px; min-height: 42px; overflow-wrap: anywhere; }
.permission-review .agent-request-btn:hover { background: var(--bg-hover); }
.permission-review .agent-request-btn.primary { background: var(--accent-color); border-color: var(--accent-color); color: var(--accent-contrast-text); }
.permission-review .agent-request-btn.primary:hover { background: var(--accent-hover); }
.permission-review .agent-request-btn:disabled { opacity: .55; cursor: not-allowed; }
.permission-review :is(button, summary, textarea, .agent-request-body):focus-visible { outline: 2px solid var(--accent-color); outline-offset: 2px; }
.agent-request-answer { display: flex; flex-direction: column; gap: 8px; margin-top: 16px; color: var(--text-secondary); }
.agent-request-answer textarea { box-sizing: border-box; width: 100%; min-height: 100px; resize: vertical; padding: 10px; background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 6px; font: inherit; }
.agent-request-error { color: var(--danger-color); overflow-wrap: anywhere; }
.agent-request-warning { margin: 0 0 12px; padding: 10px 12px; border-left: 3px solid var(--warning-color); background: var(--bg-secondary); color: var(--text-primary); font-size: 13px; white-space: pre-wrap; overflow-wrap: anywhere; }
@media (max-width: 520px), (max-height: 500px) {
.permission-review { padding: 10px; }
.permission-review .agent-request-content { max-height: calc(var(--app-height, 100dvh) - 20px); border-radius: 10px; }
.permission-review .agent-request-header { padding: 14px 16px; gap: 10px; }
.permission-review .agent-request-title { font-size: 17px; }
.permission-review .agent-request-body { padding: 16px; }
.agent-request-footer { padding: 12px 16px; }
.permission-review .permission-actions > button { flex: 1; }
.agent-request-copy { min-height: 40px; }
}
2 changes: 1 addition & 1 deletion src/vibes/static/dist/app.css

Large diffs are not rendered by default.

1,179 changes: 601 additions & 578 deletions src/vibes/static/dist/app.js

Large diffs are not rendered by default.

88 changes: 46 additions & 42 deletions src/vibes/static/dist/app.js.map

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/vibes/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>Vibes</title>
<meta name="apple-mobile-web-app-title" content="Vibes">
<link rel="stylesheet" href="/static/dist/app.css?v=2e320b413b4f80bf">
<link rel="stylesheet" href="/static/dist/app.css?v=726275fbe0e20ac4">
<link rel="manifest" href="/manifest.json">
<link id="dynamic-favicon" rel="icon" href="/static/icon-192.png" data-default="/static/icon-192.png">
<link rel="icon" type="image/png" sizes="192x192" href="/static/icon-192.png">
Expand All @@ -24,10 +24,10 @@
return false;
};
</script>
<script type="module" src="/static/dist/app.js?v=2e320b413b4f80bf"></script>
<script type="module" src="/static/dist/app.js?v=726275fbe0e20ac4"></script>
<script type="module">
// Fallback error display for module errors
import('/static/dist/app.js?v=2e320b413b4f80bf').catch(function(e) {
import('/static/dist/app.js?v=726275fbe0e20ac4').catch(function(e) {
document.getElementById('app').innerHTML = '<pre style="padding:20px;color:red;">Module Error: ' + e.message + '</pre>';
});
</script>
Expand Down
8 changes: 6 additions & 2 deletions src/vibes/static/js/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,11 +269,11 @@ export async function uploadMedia(file, { signal, onProgress } = {}) {
/**
* Respond to an agent request (permission, choice)
*/
export async function respondToAgentRequest(requestId, outcome) {
export async function respondToAgentRequest(requestId, outcome, answer) {
const response = await fetch(API_BASE + '/agent/respond', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ request_id: requestId, outcome }),
body: JSON.stringify({ request_id: requestId, outcome, ...(answer !== undefined ? { answer } : {}) }),
});

if (!response.ok) {
Expand Down Expand Up @@ -529,6 +529,10 @@ export class SSEClient {
this.onEvent('agent_request', JSON.parse(e.data));
});

this.eventSource.addEventListener('agent_request_closed', (e) => {
this.onEvent('agent_request_closed', JSON.parse(e.data));
});

this.eventSource.addEventListener('agent_request_timeout', (e) => {
this.onEvent('agent_request_timeout', JSON.parse(e.data));
});
Expand Down
Loading
Loading