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
133 changes: 79 additions & 54 deletions docs/docs/usage/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,82 +19,107 @@ altimate --agent analyst

## Subcommands

| Command | Description |
|---------|------------|
| `run` | Run a prompt non-interactively |
| `serve` | Start the HTTP API server |
| `web` | Start the web UI |
| `agent` | Agent management |
| `auth` | Authentication |
| `mcp` | Model Context Protocol tools |
| `acp` | Agent Communication Protocol |
| `models` | List available models |
| `stats` | Usage statistics |
| `export` | Export session data |
| `import` | Import session data |
| `session` | Session management |
| `trace` | List and view session traces |
| `github` | GitHub integration |
| `pr` | Pull request tools |
| `upgrade` | Upgrade to latest version |
| `uninstall` | Uninstall altimate |
| Command | Description |
| ----------- | ------------------------------ |
| `run` | Run a prompt non-interactively |
| `serve` | Start the HTTP API server |
| `web` | Start the web UI |
| `agent` | Agent management |
| `auth` | Authentication |
| `mcp` | Model Context Protocol tools |
| `acp` | Agent Communication Protocol |
| `models` | List available models |
| `stats` | Usage statistics |
| `export` | Export session data |
| `import` | Import session data |
| `session` | Session management |
| `trace` | List and view session traces |
| `github` | GitHub integration |
| `pr` | Pull request tools |
| `upgrade` | Upgrade to latest version |
| `uninstall` | Uninstall altimate |

## Global Flags

| Flag | Description |
|------|------------|
| `--model <provider/model>` | Override the default model |
| `--agent <name>` | Start with a specific agent |
| `--print-logs` | Print logs to stderr |
| `--log-level <level>` | Set log level: `DEBUG`, `INFO`, `WARN`, `ERROR` |
| `--help`, `-h` | Show help |
| `--version`, `-v` | Show version |
| Flag | Description |
| -------------------------- | ----------------------------------------------- |
| `--model <provider/model>` | Override the default model |
| `--agent <name>` | Start with a specific agent |
| `--print-logs` | Print logs to stderr |
| `--log-level <level>` | Set log level: `DEBUG`, `INFO`, `WARN`, `ERROR` |
| `--help`, `-h` | Show help |
| `--version`, `-v` | Show version |

## Environment Variables

Configuration can be controlled via environment variables:

### Core Configuration

| Variable | Description |
|----------|------------|
| `ALTIMATE_CLI_CONFIG` | Path to custom config file |
| `ALTIMATE_CLI_CONFIG_DIR` | Custom config directory |
| Variable | Description |
| ----------------------------- | ---------------------------- |
| `ALTIMATE_CLI_CONFIG` | Path to custom config file |
| `ALTIMATE_CLI_CONFIG_DIR` | Custom config directory |
| `ALTIMATE_CLI_CONFIG_CONTENT` | Inline config as JSON string |
| `ALTIMATE_CLI_GIT_BASH_PATH` | Path to Git Bash (Windows) |
| `ALTIMATE_CLI_GIT_BASH_PATH` | Path to Git Bash (Windows) |

### Feature Toggles

| Variable | Description |
|----------|------------|
| `ALTIMATE_CLI_DISABLE_AUTOUPDATE` | Disable automatic updates |
| `ALTIMATE_CLI_DISABLE_LSP_DOWNLOAD` | Don't auto-download LSP servers |
| `ALTIMATE_CLI_DISABLE_AUTOCOMPACT` | Disable automatic context compaction |
| `ALTIMATE_CLI_DISABLE_DEFAULT_PLUGINS` | Skip loading default plugins |
| `ALTIMATE_CLI_DISABLE_EXTERNAL_SKILLS` | Disable external skill discovery |
| `ALTIMATE_CLI_DISABLE_PROJECT_CONFIG` | Ignore project-level config files |
| `ALTIMATE_CLI_DISABLE_TERMINAL_TITLE` | Don't set terminal title |
| `ALTIMATE_CLI_DISABLE_PRUNE` | Disable database pruning |
| `ALTIMATE_CLI_DISABLE_MODELS_FETCH` | Don't fetch models from models.dev |
| Variable | Description |
| -------------------------------------- | ------------------------------------ |
| `ALTIMATE_CLI_DISABLE_AUTOUPDATE` | Disable automatic updates |
| `ALTIMATE_CLI_DISABLE_LSP_DOWNLOAD` | Don't auto-download LSP servers |
| `ALTIMATE_CLI_DISABLE_AUTOCOMPACT` | Disable automatic context compaction |
| `ALTIMATE_CLI_DISABLE_DEFAULT_PLUGINS` | Skip loading default plugins |
| `ALTIMATE_CLI_DISABLE_EXTERNAL_SKILLS` | Disable external skill discovery |
| `ALTIMATE_CLI_DISABLE_PROJECT_CONFIG` | Ignore project-level config files |
| `ALTIMATE_CLI_DISABLE_TERMINAL_TITLE` | Don't set terminal title |
| `ALTIMATE_CLI_DISABLE_PRUNE` | Disable database pruning |
| `ALTIMATE_CLI_DISABLE_MODELS_FETCH` | Don't fetch models from models.dev |

### Server & Security

| Variable | Description |
|----------|------------|
| Variable | Description |
| ------------------------------ | ------------------------------- |
| `ALTIMATE_CLI_SERVER_USERNAME` | Server HTTP basic auth username |
| `ALTIMATE_CLI_SERVER_PASSWORD` | Server HTTP basic auth password |
| `ALTIMATE_CLI_PERMISSION` | Permission config as JSON |
| `ALTIMATE_CLI_PERMISSION` | Permission config as JSON |

### Experimental

| Variable | Description |
|----------|------------|
| `ALTIMATE_CLI_EXPERIMENTAL` | Enable all experimental features |
| `ALTIMATE_CLI_EXPERIMENTAL_FILEWATCHER` | Enable file watcher |
| `ALTIMATE_CLI_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | Custom bash timeout (ms) |
| `ALTIMATE_CLI_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | Max output tokens |
| `ALTIMATE_CLI_EXPERIMENTAL_PLAN_MODE` | Enable plan mode |
| `ALTIMATE_CLI_ENABLE_EXA` | Enable Exa web search |
| Variable | Description |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ALTIMATE_CLI_EXPERIMENTAL` | Enable all experimental features |
| `ALTIMATE_CLI_EXPERIMENTAL_FILEWATCHER` | Enable file watcher |
| `ALTIMATE_CLI_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | Custom bash timeout (ms) |
| `ALTIMATE_CLI_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | Max output tokens |
| `ALTIMATE_CLI_EXPERIMENTAL_PLAN_MODE` | Enable plan mode |
| `ALTIMATE_CLI_ENABLE_EXA` | Enable Exa web search |
| `ALTIMATE_CALM_MODE` | Enables all streaming optimizations: smooth rendering, line-at-a-time buffering, and 100-column width cap. Recommended for a Claude Code-like experience. Equivalent to setting `ALTIMATE_SMOOTH_STREAMING=true ALTIMATE_LINE_STREAMING=true ALTIMATE_CONTENT_MAX_WIDTH=100`. |
| `ALTIMATE_SMOOTH_STREAMING` | Uses lightweight `<code>` rendering during LLM streaming, then swaps to rich markdown after completion. Reduces text jumps and scroll jitter. Included in `ALTIMATE_CALM_MODE`. |
| `ALTIMATE_LINE_STREAMING` | Buffers LLM output and reveals one complete line at a time (on `\n`). Gives a calm, steady flow. Remaining text flushes on message completion or abort. Included in `ALTIMATE_CALM_MODE`. |
| `ALTIMATE_CONTENT_MAX_WIDTH` | Cap text content width in columns (e.g. `100`). Improves readability on wide screens. Automatically disabled on small terminals. Set to `100` by `ALTIMATE_CALM_MODE`. |

#### Calm Mode Quick Start

For a Claude Code-like streaming experience, add to your shell profile:

```bash
export ALTIMATE_CALM_MODE=true
```

Or use individual flags for fine-grained control:

```bash
# Smooth rendering only (no line buffering)
export ALTIMATE_SMOOTH_STREAMING=true

# Line buffering only (no rendering changes)
export ALTIMATE_LINE_STREAMING=true

# Custom width cap (e.g., 80 columns)
export ALTIMATE_CONTENT_MAX_WIDTH=80
```

## Non-interactive Usage

Expand Down
41 changes: 41 additions & 0 deletions packages/opencode/src/cli/cmd/tui/context/sdk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2"
import { createSimpleContext } from "./helper"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { batch, onCleanup, onMount } from "solid-js"
// altimate_change start - smooth streaming
import { Flag } from "@/flag/flag"
// altimate_change end

export type EventSource = {
on: (handler: (event: Event) => void) => () => void
Expand Down Expand Up @@ -48,6 +51,44 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
queue = []
timer = undefined
last = Date.now()

// altimate_change start - smooth streaming: pre-merge delta events
// When enabled, merge consecutive delta events for the same part+field
// to reduce store updates from N-per-part to 1-per-part per flush cycle.
if (Flag.ALTIMATE_SMOOTH_STREAMING) {
const merged: Event[] = []
const deltaMap = new Map<string, number>()
for (const event of events) {
if (event.type === "message.part.delta") {
const props = event.properties as { messageID: string; partID: string; field: string; delta: string }
const key = `${props.messageID}:${props.partID}:${props.field}`
const existing = deltaMap.get(key)
if (existing !== undefined) {
const prev = merged[existing] as typeof event
merged[existing] = {
...prev,
properties: {
...prev.properties,
delta: (prev.properties as typeof props).delta + props.delta,
},
} as Event
continue
}
deltaMap.set(key, merged.length)
} else {
deltaMap.clear()
}
merged.push(event)
}
Comment on lines +64 to +82

This comment was marked as outdated.

batch(() => {
for (const event of merged) {
emitter.emit(event.type, event)
}
})
return
}
// altimate_change end

// Batch all event emissions so all store updates result in a single render
batch(() => {
for (const event of events) {
Expand Down
117 changes: 107 additions & 10 deletions packages/opencode/src/cli/cmd/tui/context/sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,43 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
setStore("workspaceList", reconcile(result.data))
}

// altimate_change start - line streaming: buffer deltas, flush only on \n or message completion
const lineBuffer = new Map<string, string>()

function flushLineBuffer(messageID: string, partID: string, field: string, forceAll: boolean) {
const key = `${messageID}:${partID}:${field}`
const buffer = lineBuffer.get(key)
if (!buffer) return
let textToFlush: string
if (forceAll) {
textToFlush = buffer
lineBuffer.delete(key)
} else {
const lastNewline = buffer.lastIndexOf("\n")
if (lastNewline === -1) return
textToFlush = buffer.slice(0, lastNewline + 1)
const remainder = buffer.slice(lastNewline + 1)
if (remainder) lineBuffer.set(key, remainder)
else lineBuffer.delete(key)
}
if (!textToFlush) return
const parts = store.part[messageID]
if (!parts) return
const result = Binary.search(parts, partID, (p) => p.id)
if (!result.found) return
const existing = parts[result.index][field as keyof (typeof parts)[number]] as string | undefined
setStore("part", messageID, result.index, field as any, ((existing ?? "") + textToFlush) as any)
}

function flushAllBuffersForMessage(messageID: string) {
for (const [key] of lineBuffer) {
if (!key.startsWith(messageID + ":")) continue
const [, partID, field] = key.split(":")
flushLineBuffer(messageID, partID, field, true)
}
}
// altimate_change end

sdk.event.listen((e) => {
const event = e.details
switch (event.type) {
Expand Down Expand Up @@ -254,6 +291,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
}

case "message.updated": {
// altimate_change start - line streaming: flush remaining buffer when message completes
if (
Flag.ALTIMATE_LINE_STREAMING &&
"completed" in event.properties.info.time &&
event.properties.info.time.completed
) {
flushAllBuffersForMessage(event.properties.info.id)
}
// altimate_change end
const messages = store.message[event.properties.info.sessionID]
if (!messages) {
setStore("message", event.properties.info.sessionID, [event.properties.info])
Expand Down Expand Up @@ -293,6 +339,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
break
}
case "message.removed": {
// altimate_change start - line streaming: clean up buffers for removed/aborted messages
if (Flag.ALTIMATE_LINE_STREAMING) {
flushAllBuffersForMessage(event.properties.messageID)
}
// altimate_change end
const messages = store.message[event.properties.sessionID]
const result = Binary.search(messages, event.properties.messageID, (m) => m.id)
if (result.found) {
Expand All @@ -307,6 +358,17 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
break
}
case "message.part.updated": {
// altimate_change start - line streaming: discard buffered text when part is
// authoritatively set by the server (via reconcile). Without this, the buffer
// would append stale text on top of the server's complete content, duplicating
// the trailing partial line.
if (Flag.ALTIMATE_LINE_STREAMING) {
const { messageID, id: partID } = event.properties.part
for (const key of lineBuffer.keys()) {
if (key.startsWith(`${messageID}:${partID}:`)) lineBuffer.delete(key)
}
}
// altimate_change end
const parts = store.part[event.properties.part.messageID]
if (!parts) {
setStore("part", event.properties.part.messageID, [event.properties.part])
Expand All @@ -332,20 +394,55 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
if (!parts) break
const result = Binary.search(parts, event.properties.partID, (p) => p.id)
if (!result.found) break
setStore(
"part",
event.properties.messageID,
produce((draft) => {
const part = draft[result.index]
const field = event.properties.field as keyof typeof part
const existing = part[field] as string | undefined
;(part[field] as string) = (existing ?? "") + event.properties.delta
}),
)
// altimate_change start - line streaming: buffer deltas, flush only on \n
// Note: when line streaming is enabled (including via calm mode), this branch
// handles all delta processing and breaks — the smooth streaming branch below
// is not reached. This is intentional: flushLineBuffer already does direct
// store path updates, so the produce() bypass is not needed.
if (Flag.ALTIMATE_LINE_STREAMING) {
const { messageID, partID, field, delta } = event.properties
const key = `${messageID}:${partID}:${field}`
lineBuffer.set(key, (lineBuffer.get(key) ?? "") + delta)
flushLineBuffer(messageID, partID, field, false)
break
}
// altimate_change end
// altimate_change start - smooth streaming: direct path update avoids produce() proxy overhead
if (Flag.ALTIMATE_SMOOTH_STREAMING) {
const field = event.properties.field as keyof (typeof parts)[number]
const existing = parts[result.index][field] as string | undefined
setStore(
"part",
event.properties.messageID,
result.index,
field as any,
((existing ?? "") + event.properties.delta) as any,
)
} else {
setStore(
"part",
event.properties.messageID,
produce((draft) => {
const part = draft[result.index]
const field = event.properties.field as keyof typeof part
const existing = part[field] as string | undefined
;(part[field] as string) = (existing ?? "") + event.properties.delta
}),
)
}
// altimate_change end
break
}

case "message.part.removed": {
// altimate_change start - line streaming: discard buffers for removed parts
if (Flag.ALTIMATE_LINE_STREAMING) {
const { messageID, partID } = event.properties
for (const key of lineBuffer.keys()) {
if (key.startsWith(`${messageID}:${partID}:`)) lineBuffer.delete(key)
}
}
// altimate_change end
const parts = store.part[event.properties.messageID]
const result = Binary.search(parts, event.properties.partID, (p) => p.id)
if (result.found)
Expand Down
Loading
Loading