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
14 changes: 14 additions & 0 deletions docs/architecture/terminal-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,20 @@ Approvals and denials call the network-grant REST endpoints directly instead of
- Drag across transcript text to select it; releasing the mouse automatically copies the selected plain text to the system clipboard
- `astonish chat --resume <id>` — loads history via `GET /api/studio/sessions/{id}` on open

### Paste handling

Pastes of up to three lines are inserted into the composer as-is and the composer grows vertically for multi-line typed input (up to four visible rows). Pasting four or more lines replaces the composer value with a compact placeholder such as `[Pasted: 8 lines]`, storing the full content for submit. Explicit paste events (`Ctrl+V`, bracketed paste, clipboard paste bindings) collapse immediately. macOS `Command+V` is usually injected by the terminal as ordinary input rather than a paste event; that path collapses as soon as the composer reaches four lines (without waiting for a later keystroke). Any trailing characters that still arrive in the same short paste stream are merged into the same paste block and the placeholder line count is updated. Explicit multi-line editing with `Shift+Enter` / `Alt+Enter` / `Ctrl+J` is left expanded. Pressing enter expands the placeholder back to the full pasted content for history, transcript, `@file` expansion, and the platform turn. Each `[Pasted: N lines]` token is atomic: left/right (and word-motion) keys jump over the whole token, typing cannot insert inside it, and Backspace / `Ctrl+W` / `Alt+Backspace` / Delete remove the entire token in one step.

### Image paste

Pasting an image from the system clipboard (`Ctrl+V` / Super+V, and empty text pastes that only contain image data) inserts an atomic placeholder such as `[image #1]`. Multiple images increment the index (`#2`, `#3`, …). Image tokens share the same atomic navigation/delete behavior as text-paste placeholders. On submit, remaining image tokens are sent to Studio as multimodal `attachments` (base64 PNG/JPEG/GIF) while the user transcript keeps the `[image #N]` markers.

Clipboard image reads are platform-specific:

- **macOS**: `osascript` pasteboard (`PNGf`, with TIFF→PNG via `sips`)
- **Linux**: Wayland `wl-paste --type image/*` first, then X11 `xclip -t image/*`
- Other platforms: image clipboard read is unavailable (text paste still works)

### `@file` mentions

Typing `@` plus part of a local relative path opens a fuzzy file picker above the composer. Selecting a file inserts `@path/to/file`. On submit, the terminal app reads each mentioned file from the current working directory and appends a bounded `<context from @file mentions>` section to the message sent to the platform, while the transcript keeps showing the user's original text. Absolute paths, directory mentions, workspace escapes, and oversized files are rejected before the turn is sent.
Expand Down
8 changes: 8 additions & 0 deletions docs/website/docs/cli/chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ If the pinned provider's credential is revoked or unavailable, the session still

The terminal footer shows the active provider and the resolved concrete model when the platform reports them. It does not show `default` as the model name; while the cascade is still resolving, it shows the provider with `model resolving…` instead.

## Paste behavior

Pasting up to three lines inserts the text directly into the composer. Pasting four or more lines keeps the composer compact by showing a placeholder like `[Pasted: 8 lines]`. The full pasted content is restored when you press `Enter`, so the conversation history and the agent both receive the complete text. The placeholder is atomic: arrow keys jump over it, you cannot type inside it, and Backspace / `Ctrl+W` / `Alt+Backspace` remove the whole block at once.

Pasting an image from the clipboard inserts `[image #1]` (then `#2`, `#3`, …). Image placeholders are also atomic tokens. When you press `Enter`, Astonish sends the images to the platform as chat attachments so multimodal models can see them, while the chat history keeps the `[image #N]` markers.

On Linux, image paste requires `wl-paste` (Wayland) or `xclip` (X11) on `PATH`. On macOS, the system pasteboard is used directly.

## In-Session Commands

While in an active chat session, type `/` to access these commands:
Expand Down
18 changes: 13 additions & 5 deletions pkg/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,13 +196,21 @@ func (c *Client) GetEffectiveProviders() (*EffectiveProvidersResponse, error) {
return &resp, nil
}

// ChatAttachment is a base64 file payload for multimodal chat turns.
type ChatAttachment struct {
Filename string `json:"filename"`
MimeType string `json:"mimeType"`
Data string `json:"data"` // base64-encoded file content
}

// ChatRequest represents a message to send to the chat.
type ChatRequest struct {
SessionID string `json:"sessionId,omitempty"`
Message string `json:"message"`
AutoApprove bool `json:"autoApprove,omitempty"`
Debug bool `json:"debug,omitempty"`
SystemContext string `json:"systemContext,omitempty"`
SessionID string `json:"sessionId,omitempty"`
Message string `json:"message"`
AutoApprove bool `json:"autoApprove,omitempty"`
Debug bool `json:"debug,omitempty"`
SystemContext string `json:"systemContext,omitempty"`
Attachments []ChatAttachment `json:"attachments,omitempty"`
}

// SendChatMessage sends a chat message and returns an SSE stream of events.
Expand Down
39 changes: 39 additions & 0 deletions pkg/launcher/tui_chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package launcher

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"path"
"strings"
"sync"

Expand Down Expand Up @@ -384,6 +386,7 @@ func (b *platformBackend) RunTurn(ctx context.Context, message string, opts back
AutoApprove: autoApprove,
Debug: debug,
SystemContext: opts.SystemContext,
Attachments: chatAttachmentsFromBackend(opts.Attachments),
}

stream, err := c.SendChatMessage(req)
Expand Down Expand Up @@ -784,6 +787,42 @@ func containsRune(s string, r rune) bool {
return false
}

func chatAttachmentsFromBackend(atts []backend.Attachment) []client.ChatAttachment {
if len(atts) == 0 {
return nil
}
out := make([]client.ChatAttachment, 0, len(atts))
for _, a := range atts {
if len(a.Data) == 0 {
continue
}
filename := a.Filename
if filename == "" {
ext := "bin"
switch a.MimeType {
case "image/png":
ext = "png"
case "image/jpeg":
ext = "jpg"
case "image/gif":
ext = "gif"
case "image/webp":
ext = "webp"
}
filename = path.Base("attachment." + ext)
}
out = append(out, client.ChatAttachment{
Filename: filename,
MimeType: a.MimeType,
Data: base64.StdEncoding.EncodeToString(a.Data),
})
}
if len(out) == 0 {
return nil
}
return out
}

func firstNonEmpty(vals ...string) string {
for _, v := range vals {
if v != "" {
Expand Down
Loading
Loading