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:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 21:03:26 +01:00
1 parent 013695722a
commit a0d0e707f2
48 files changed
+2724 -228

No files matched your search

+42 -2
View File
@@ -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" };