Add Web Push, guided start screen, SVG icons, chunked uploads, rename
- Web Push (VAPID + RFC 8291, pure Rust): permission/question/finished notifications while no app window is visible; settings with test button - Guided new-session flow: task, craft target (website/app/game/old software/fresh), clean room or deep, description, model, folder (new, existing or cloned git repository); originals upload to original/ - Craft phase bar (craft_phase tool) for study/specify/build/check/ready - Chunked uploads (8 MB) up to server.max_upload_mb (default 500 MB) - All UI glyphs and emojis replaced by themeable SVG icons - Rename dialog with manual name or model-generated suggestion Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
013695722a
commit
a0d0e707f2
48 files changed
+2724
-228
No files matched your search
+42
-2
@@ -220,7 +220,7 @@ pub fn available_tools(app: &App, agent: &Agent, owner: Option<&crate::store::Us
|
||||
all.into_iter()
|
||||
.filter(|t| !agent.read_only || t.read_only())
|
||||
// Craft tools only where they are useful.
|
||||
.filter(|t| agent.name == "craft" || !matches!(t.name(), "browser_snapshot" | "image_compare" | "binary_info" | "decompile") || agent.tools.is_some())
|
||||
.filter(|t| agent.name == "craft" || !matches!(t.name(), "browser_snapshot" | "image_compare" | "binary_info" | "decompile" | "craft_phase") || agent.tools.is_some())
|
||||
.filter(|t| match &agent.tools {
|
||||
None => true,
|
||||
Some(list) => {
|
||||
@@ -640,6 +640,7 @@ fn create_child(app: &Arc<App>, ctx: &ToolCtx, agent_name: &str, description: &s
|
||||
todos: Value::Null,
|
||||
last_usage: None,
|
||||
clean_room: parent.clean_room,
|
||||
phase: None,
|
||||
};
|
||||
let child = app.save_session(child)?;
|
||||
let msg = Message::new(Role::User, vec![Part::Text { text: prompt }]);
|
||||
@@ -929,6 +930,45 @@ fn maybe_generate_title(app: Arc<App>, session_id: &str, text: &str) {
|
||||
|
||||
const COMPACT_PROMPT: &str = "Summarize this coding session so far so that the work can continue seamlessly from the summary alone. Include: the user's goals and explicit requests (verbatim where important), decisions made, files created/changed (with paths) and their current state, commands/tests run and results, open problems, and the exact next steps. Be thorough but concise; use headings and bullet points.";
|
||||
|
||||
/// Suggest a title for a session from its conversation, with the given model
|
||||
/// (or the small model). The title is returned, not saved.
|
||||
pub async fn suggest_title(app: Arc<App>, session_id: &str, model_spec: Option<String>) -> Result<String> {
|
||||
let session = app.session(session_id)?;
|
||||
let spec = model_spec.filter(|m| !m.is_empty()).unwrap_or_else(|| small_model(&app, &session));
|
||||
let (provider, mut model) = app.registry().resolve(&spec)?;
|
||||
model.effort = Some("low".into());
|
||||
let history = active_history(app.store.messages(session_id)?);
|
||||
let mut transcript = transcript_of(&history);
|
||||
if transcript.len() > 12_000 {
|
||||
// Beginning (the goal) and end (where it went).
|
||||
let head = clip(&transcript, 6_000);
|
||||
let mut start = transcript.len() - 5_000;
|
||||
while !transcript.is_char_boundary(start) {
|
||||
start += 1;
|
||||
}
|
||||
transcript = format!("{head}\n…\n{}", &transcript[start..]);
|
||||
}
|
||||
if transcript.trim().is_empty() {
|
||||
bail!("the session has no messages yet");
|
||||
}
|
||||
let req = ChatRequest {
|
||||
system: "Write a short, specific title (max 6 words) for the coding session below. Use the language of the user's messages. Reply with the title only, no quotes, no punctuation at the end.".into(),
|
||||
messages: vec![Message::new(Role::User, vec![Part::Text { text: format!("<session>\n{transcript}\n</session>") }])],
|
||||
tools: vec![],
|
||||
max_tokens: 2000,
|
||||
reasoning: false,
|
||||
model: model.clone(),
|
||||
};
|
||||
let (text, usage) = complete_text(&app, &spec, provider, req, CancellationToken::new()).await?;
|
||||
record_side_usage(&app, &session, &spec, &model, &usage, "title");
|
||||
let text = strip_think(&text);
|
||||
let title = text.lines().find(|l| !l.trim().is_empty()).unwrap_or("").trim().trim_matches(['"', '\'', '*', '#', ' ']).to_string();
|
||||
if title.is_empty() {
|
||||
bail!("the model returned no title");
|
||||
}
|
||||
Ok(title.chars().take(80).collect())
|
||||
}
|
||||
|
||||
/// Remove a leading `<think>…</think>` block some models put into the text.
|
||||
fn strip_think(text: &str) -> String {
|
||||
let t = text.trim_start();
|
||||
@@ -953,7 +993,7 @@ fn clip(s: &str, max: usize) -> String {
|
||||
}
|
||||
|
||||
/// The history as readable text for summarizing (tool output shortened).
|
||||
fn transcript_of(history: &[Message]) -> String {
|
||||
pub fn transcript_of(history: &[Message]) -> String {
|
||||
let mut out = String::new();
|
||||
for m in history {
|
||||
let role = if matches!(m.role, Role::User) { "USER" } else { "ASSISTANT" };
|
||||
|
||||
Reference in new issue
Block a user