Skip to content

Commit 0cb8ca0

Browse files
authored
refactor: family providers, fix compose cursor, update models (#32)
refactor: family providers, fix compose cursor, update models
2 parents 5875139 + caa183f commit 0cb8ca0

7 files changed

Lines changed: 437 additions & 160 deletions

File tree

Cargo.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "isanagent"
3-
version = "0.4.0"
3+
version = "0.5.0"
44
edition = "2021"
55
license = "Apache-2.0"
66
repository = "https://github.com/altaidevorg/isanagent"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Invoke-WebRequest https://github.com/altaidevorg/isanagent/releases/download/mai
7979

8080
If you use the **default workspace** (`~/.isanagent` on Unix, or the equivalent on Windows) and that folder does not exist yet, **the first run starts the interactive onboard wizard** (provider, API key env var, model, and workspace layout), then continues into the agent in the same session. For a custom workspace path, run `isanagent onboard` (add `--interactive` for the full wizard) or `isanagent --workspace /path/to/workspace` once the directory and `config.toml` exist.
8181

82-
Set API credentials the wizard recommends (for example `GEMINI_API_KEY` or your providers variable). Turn on **`[api] enabled = true`** and **`serve_ui = true`** in `config.toml` when you want the browser UI on `http://127.0.0.1:<port>/`. For channels, memory, harness options, and sandbox rules, see [`AGENTS.md`](./AGENTS.md).
82+
Set API credentials the wizard recommends (for example `GEMINI_API_KEY` or your provider's variable). Turn on **`[api] enabled = true`** and **`serve_ui = true`** in `config.toml` when you want the browser UI on `http://127.0.0.1:<port>/`. For channels, memory, harness options, and sandbox rules, see [`AGENTS.md`](./AGENTS.md).
8383

8484
### Multi-provider setup
8585

assets/onboarding/config.toml

Lines changed: 29 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -14,120 +14,35 @@ enabled = true
1414
# LLM providers. At startup, isanagent picks the first provider with a valid API key.
1515
# Use `/model` in the TUI to switch between them at runtime.
1616
#
17-
# `provider_name` selects a built-in registry entry (anthropic, gemini, openai, deepseek,
18-
# openrouter). API key resolution order:
19-
# 1. Environment variable ($<PROVIDER>_API_KEY, e.g. $OPENAI_API_KEY for "openai")
20-
# 2. Direct `api_key` value in config (fallback when env var is not set)
21-
# Override the env var name with `api_key_env` if needed.
22-
#
23-
# Set `provider_name = "openai_compatible"` + `base_url` for any other endpoint.
24-
#
25-
# ─── Anthropic (Claude) ─── $ANTHROPIC_API_KEY or api_key below ───────────────
26-
#
27-
# [providers.claude-opus-4-7]
28-
# provider_name = "anthropic"
29-
# model_name = "claude-opus-4-7"
30-
# api_key = "sk-ant-..." # optional: omit to use $ANTHROPIC_API_KEY
31-
#
32-
# [providers.claude-opus-4-6]
33-
# provider_name = "anthropic"
34-
# model_name = "claude-opus-4-6"
35-
#
36-
# [providers.claude-sonnet-4-6]
37-
# provider_name = "anthropic"
38-
# model_name = "claude-sonnet-4-6"
39-
#
40-
# [providers.claude-haiku-4-5]
41-
# provider_name = "anthropic"
42-
# model_name = "claude-haiku-4-5-20251001"
43-
#
44-
# ─── OpenAI ─── $OPENAI_API_KEY or api_key below ─────────────────────────────
45-
#
46-
# [providers.gpt-5-5]
47-
# provider_name = "openai"
48-
# model_name = "gpt-5.5"
49-
# api_key = "sk-..." # optional: omit to use $OPENAI_API_KEY
50-
#
51-
# [providers.gpt-5-4-mini]
52-
# provider_name = "openai"
53-
# model_name = "gpt-5.4-mini"
54-
#
55-
# [providers.gpt-5-4-nano]
56-
# provider_name = "openai"
57-
# model_name = "gpt-5.4-nano"
58-
#
59-
# [providers.gpt-4-1]
60-
# provider_name = "openai"
61-
# model_name = "gpt-4.1"
62-
#
63-
# [providers.gpt-4-1-mini]
64-
# provider_name = "openai"
65-
# model_name = "gpt-4.1-mini"
66-
#
67-
# [providers.gpt-4o]
68-
# provider_name = "openai"
69-
# model_name = "gpt-4o"
70-
#
71-
# [providers.o3]
72-
# provider_name = "openai"
73-
# model_name = "o3"
74-
#
75-
# [providers.o4-mini]
76-
# provider_name = "openai"
77-
# model_name = "o4-mini"
78-
#
79-
# ─── Google Gemini ─── $GEMINI_API_KEY or api_key below ───────────────────────
80-
#
81-
# [providers.gemini-3-1-pro]
82-
# provider_name = "gemini"
83-
# model_name = "gemini-3.1-pro-preview"
84-
# api_key = "AIza..."
85-
#
86-
# [providers.gemini-3-flash]
87-
# provider_name = "gemini"
88-
# model_name = "gemini-3-flash-preview"
89-
#
90-
# [providers.gemini-2-5-pro]
91-
# provider_name = "gemini"
92-
# model_name = "gemini-2.5-pro"
93-
#
94-
# [providers.gemini-2-5-flash]
95-
# provider_name = "gemini"
96-
# model_name = "gemini-2.5-flash"
97-
#
98-
# [providers.gemini-2-5-flash-lite]
99-
# provider_name = "gemini"
100-
# model_name = "gemini-2.5-flash-lite"
101-
#
102-
# ─── DeepSeek ─── $DEEPSEEK_API_KEY or api_key below ─────────────────────────
103-
#
104-
# [providers.deepseek-v4-pro]
105-
# provider_name = "deepseek"
106-
# model_name = "deepseek-v4-pro"
107-
# api_key = "sk-..." # optional: omit to use $DEEPSEEK_API_KEY
108-
#
109-
# [providers.deepseek-v4-flash]
110-
# provider_name = "deepseek"
111-
# model_name = "deepseek-v4-flash"
112-
#
113-
# [providers.deepseek-reasoner]
114-
# provider_name = "deepseek"
115-
# model_name = "deepseek-reasoner"
116-
#
117-
# ─── OpenRouter (unified gateway) ─── $OPENROUTER_API_KEY or api_key below ───
118-
#
119-
# [providers.or-claude-opus]
120-
# provider_name = "openrouter"
121-
# model_name = "anthropic/claude-opus-4-7"
122-
# api_key = "sk-or-..." # optional: omit to use $OPENROUTER_API_KEY
123-
#
124-
# [providers.or-gpt-5-5]
125-
# provider_name = "openrouter"
126-
# model_name = "openai/gpt-5.5"
127-
#
128-
# [providers.or-gemini-3-1-pro]
129-
# provider_name = "openrouter"
130-
# model_name = "google/gemini-3.1-pro-preview"
17+
# Family format: list all models under one provider block. The map key is used as provider_name.
18+
# API key resolution: $<PROVIDER>_API_KEY env var first, then inline `api_key` as fallback.
19+
#
20+
[providers.gemini]
21+
models = ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-3.1-pro", "gemini-3.1-flash-lite"]
22+
api_key = "<changethis>"
23+
24+
[providers.openai]
25+
models = ["gpt-5.5", "gpt-5.5-instant", "gpt-5.4-mini", "gpt-5.4-nano", "o3", "o4-mini"]
26+
api_key = "<changethis>"
27+
28+
[providers.anthropic]
29+
models = ["claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5-20251001"]
30+
api_key = "<changethis>"
31+
32+
[providers.deepseek]
33+
models = ["deepseek-v4-pro", "deepseek-v4-flash"]
34+
api_key = "<changethis>"
35+
36+
# [providers.openrouter]
37+
# models = ["anthropic/claude-opus-4-7", "openai/gpt-5.5", "google/gemini-3.1-pro"]
38+
# # Uses $OPENROUTER_API_KEY
39+
#
40+
# Custom endpoint (legacy per-model format):
41+
# [providers.my-proxy]
42+
# provider_name = "openai_compatible"
43+
# model_name = "my-model"
44+
# base_url = "https://my-proxy.example/v1/chat/completions"
45+
# api_key = "sk-..."
13146

13247
[slack]
13348
enabled = false

src/channels/terminal_ui/run.rs

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,75 @@ fn append_execution_stream_panel(app: &mut App, msg: &OutboundMessage) {
880880
}
881881
}
882882

883+
/// Character-level wrapping for the compose input box.
884+
///
885+
/// Splits each logical line (delimited by `'\n'` in `input`) into visual rows
886+
/// that are at most `width` display-columns wide. The first logical line gets
887+
/// the `"> "` prompt; continuations get `" "`. Each row is a complete
888+
/// [`Line`] so the caller can render the result **without** `Wrap`.
889+
///
890+
/// This keeps cursor-position arithmetic (`total_cols / width`) perfectly
891+
/// aligned with what is actually drawn on screen.
892+
fn char_wrap_compose(
893+
input: &str,
894+
width: usize,
895+
prompt_style: Style,
896+
text_style: Style,
897+
) -> Vec<Line<'static>> {
898+
use unicode_width::UnicodeWidthChar;
899+
900+
let w = width.max(1);
901+
let mut out: Vec<Line<'static>> = Vec::new();
902+
903+
for (li, logical) in input.split('\n').enumerate() {
904+
let prefix: &str = if li == 0 { "> " } else { " " };
905+
let prefix_w: usize = 2; // both "> " and " " are 2 display-columns
906+
let text_budget = w.saturating_sub(prefix_w);
907+
908+
// First visual row: prefix + as much text as fits.
909+
let mut chars = logical.chars().peekable();
910+
let mut first_chunk = String::new();
911+
let mut col: usize = 0;
912+
while let Some(&ch) = chars.peek() {
913+
let cw = UnicodeWidthChar::width(ch).unwrap_or(0).max(1);
914+
if col + cw > text_budget {
915+
break;
916+
}
917+
first_chunk.push(ch);
918+
col += cw;
919+
chars.next();
920+
}
921+
out.push(Line::from(vec![
922+
Span::styled(prefix.to_string(), prompt_style),
923+
Span::styled(first_chunk, text_style),
924+
]));
925+
926+
// Subsequent visual rows: up to `w` columns of text each (no prefix).
927+
while chars.peek().is_some() {
928+
let mut chunk = String::new();
929+
let mut col: usize = 0;
930+
while let Some(&ch) = chars.peek() {
931+
let cw = UnicodeWidthChar::width(ch).unwrap_or(0).max(1);
932+
if col + cw > w {
933+
break;
934+
}
935+
chunk.push(ch);
936+
col += cw;
937+
chars.next();
938+
}
939+
if chunk.is_empty() {
940+
// Single character wider than the whole line — force it through.
941+
if let Some(ch) = chars.next() {
942+
chunk.push(ch);
943+
}
944+
}
945+
out.push(Line::from(Span::styled(chunk, text_style)));
946+
}
947+
}
948+
949+
out
950+
}
951+
883952
fn rect_contains(r: Rect, col: u16, row: u16) -> bool {
884953
let x1 = r.x.saturating_add(r.width);
885954
let y1 = r.y.saturating_add(r.height);
@@ -1620,24 +1689,20 @@ pub(crate) fn run_ratatui_main(config: RatatuiMainConfig) -> io::Result<()> {
16201689
.title(Span::styled(" compose ", Theme::dim()))
16211690
.border_style(Theme::dim());
16221691

1623-
let mut text_lines = Vec::new();
1624-
for (i, line) in app.input.split('\n').enumerate() {
1625-
let prefix = if i == 0 { "> " } else { " " };
1626-
text_lines.push(Line::from(vec![
1627-
Span::styled(prefix, Theme::input_prompt()),
1628-
Span::styled(line, Theme::text()),
1629-
]));
1630-
}
1631-
if text_lines.is_empty() {
1632-
text_lines.push(Line::from(vec![
1633-
Span::styled("> ", Theme::input_prompt()),
1634-
Span::styled("", Theme::text()),
1635-
]));
1636-
}
1637-
16381692
let inner_area = ch[5].inner(Margin::new(1, 1));
16391693
let inner_w = inner_area.width.max(1); // guard against zero-width after margin
16401694

1695+
// Build visual lines with character-level wrapping so that the cursor
1696+
// calculation (`total_cols / inner_w`) matches the rendered layout
1697+
// exactly. Previously the code relied on ratatui's Wrap which uses
1698+
// word-level breaking, causing a mismatch.
1699+
let text_lines = char_wrap_compose(
1700+
&app.input,
1701+
inner_w as usize,
1702+
Theme::input_prompt(),
1703+
Theme::text(),
1704+
);
1705+
16411706
// Calculate visual line of cursor for wrapping.
16421707
// Uses display_width (Unicode column width) instead of char count so that
16431708
// CJK characters and emoji that occupy two cells are measured correctly.
@@ -1647,7 +1712,7 @@ pub(crate) fn run_ratatui_main(config: RatatuiMainConfig) -> io::Result<()> {
16471712
let lines_before_cursor: Vec<&str> = text_before_cursor.split('\n').collect();
16481713
let total_lines = lines_before_cursor.len();
16491714
for (i, line) in lines_before_cursor.into_iter().enumerate() {
1650-
let prefix_len: u16 = if i == 0 { 2 } else { 0 }; // "> "
1715+
let prefix_len: u16 = 2; // "> " on first line, " " on continuations
16511716
let col_width = super::display_width(line) as u16;
16521717
let total_cols = prefix_len + col_width;
16531718

@@ -1672,7 +1737,6 @@ pub(crate) fn run_ratatui_main(config: RatatuiMainConfig) -> io::Result<()> {
16721737

16731738
let input_para = Paragraph::new(Text::from(text_lines))
16741739
.block(input_block)
1675-
.wrap(ratatui::widgets::Wrap { trim: false })
16761740
.scroll((input_v_scroll, 0));
16771741
f.render_widget(input_para, ch[5]);
16781742

0 commit comments

Comments
 (0)