Fix stale PWA service worker after updates; collapsible task list; short screens
Installed apps checked their service worker at /sw-<old build>.js. That file no longer exists after an update and the static fallback answered with index.html, so the update check failed and the old worker kept serving the previous app shell. The old frontend crashed on the new image references (base64Size of a missing `data`), leaving a blank page on phones. Old service worker URLs now get the clean-up worker (/sw.js), which unregisters and reloads; missing scripts and styles answer 404 instead of HTML. - Task list above the composer folds to one line (progress, current task), remembered per device. - max-height: 560px layout (square phones such as the Clicks Communicator, landscape): tighter top bar, composer and new-chat wizard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b1fa54a7b6
commit
d7f89ee257
6 files changed
+232
-8
No files matched your search
@@ -3,6 +3,24 @@
|
||||
All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/);
|
||||
until 1.0, minor versions may contain breaking changes.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- **Collapsible task list:** the model's task list above the composer folds to one line
|
||||
(progress and current task) with a click on its heading; the choice is kept per device.
|
||||
- **Square and short screens** (e.g. Clicks Communicator, phones in landscape): tighter top bar,
|
||||
composer and new-chat wizard, so the chat keeps most of the height.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Blank page on phones after the update:** installed apps (PWA) checked their service worker
|
||||
at its old URL, got the web page instead of a script and therefore kept the previous app
|
||||
version forever. With the new image format of v202611-00010 that old version crashed after
|
||||
showing the chat for a moment. Old service worker URLs now get the clean-up worker, which
|
||||
removes the stale worker and reloads the current version. Missing scripts/styles answer
|
||||
404 instead of the web page.
|
||||
|
||||
## [v202611-00010] – 2026-11-09
|
||||
|
||||
### Changed
|
||||
|
||||
+34
-2
@@ -172,8 +172,7 @@ pub fn router(app: AppState) -> Router {
|
||||
.find(|p| p.join("index.html").exists())
|
||||
.unwrap_or_else(|| PathBuf::from("./web/dist"))
|
||||
});
|
||||
let index = web_dir.join("index.html");
|
||||
let static_files = tower_http::services::ServeDir::new(&web_dir).fallback(tower_http::services::ServeFile::new(index));
|
||||
let static_files = tower_http::services::ServeDir::new(&web_dir).fallback(missing_static_file(web_dir.clone()));
|
||||
|
||||
Router::new()
|
||||
.nest("/api", api)
|
||||
@@ -186,6 +185,39 @@ pub fn router(app: AppState) -> Router {
|
||||
.with_state(app)
|
||||
}
|
||||
|
||||
/// Paths that are not a file of the web build. Usually a client-side route → index.html,
|
||||
/// except for:
|
||||
/// - `/sw-<build>.js` of an earlier build: installed apps check their service worker at
|
||||
/// that URL for updates. Answering with index.html (not a script) makes the update
|
||||
/// fail, so the old worker kept serving the old app shell forever – which broke when
|
||||
/// the API changed. They get the kill switch /sw.js instead: it removes the old worker
|
||||
/// and reloads, and the page then registers the current worker.
|
||||
/// - other missing scripts/styles (old chunks): 404 instead of HTML.
|
||||
fn missing_static_file(web_dir: PathBuf) -> axum::routing::MethodRouter {
|
||||
use axum::http::{header, StatusCode};
|
||||
use axum::response::IntoResponse;
|
||||
axum::routing::any(move |uri: axum::http::Uri| {
|
||||
let web_dir = web_dir.clone();
|
||||
async move {
|
||||
let path = uri.path();
|
||||
let name = path.trim_start_matches('/');
|
||||
if name.starts_with("sw-") && name.ends_with(".js") && !name.contains('/') {
|
||||
return match tokio::fs::read(web_dir.join("sw.js")).await {
|
||||
Ok(js) => ([(header::CONTENT_TYPE, "text/javascript"), (header::CACHE_CONTROL, "no-cache, no-store")], js).into_response(),
|
||||
Err(_) => StatusCode::NOT_FOUND.into_response(),
|
||||
};
|
||||
}
|
||||
if [".js", ".mjs", ".css", ".map"].iter().any(|ext| path.ends_with(ext)) {
|
||||
return StatusCode::NOT_FOUND.into_response();
|
||||
}
|
||||
match tokio::fs::read(web_dir.join("index.html")).await {
|
||||
Ok(html) => ([(header::CONTENT_TYPE, "text/html; charset=utf-8"), (header::CACHE_CONTROL, "no-cache")], html).into_response(),
|
||||
Err(_) => StatusCode::NOT_FOUND.into_response(),
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// gzip/brotli for JSON, HTML, JS and CSS. Images and SSE streams are left alone (default
|
||||
/// predicate), and so are binary downloads (already compressed or large).
|
||||
fn compression() -> tower_http::compression::CompressionLayer<impl tower_http::compression::Predicate + Clone> {
|
||||
|
||||
@@ -7,9 +7,9 @@ import { useModels } from "../hooks";
|
||||
import { fmtCompact, fmtCost, fmtNumber, fmtPercent, useT, type Key } from "../i18n";
|
||||
import { navigate } from "../router";
|
||||
import { dismissError, loadSession, loadSessions, useStore } from "../store";
|
||||
import type { Aggregate, ImageData, Message, Part, Session } from "../types";
|
||||
import type { Aggregate, ImageData, Message, Part, Session, Todo } from "../types";
|
||||
import { ActivityLine, Dots, Spinner, ThinkIcon, WorkBar } from "./Activity";
|
||||
import { useReasoningMode } from "../prefs";
|
||||
import { useReasoningMode, useTodosCollapsed } from "../prefs";
|
||||
import { Composer } from "./Composer";
|
||||
import { ShareDialog } from "./ShareDialog";
|
||||
import { RenameDialog } from "./RenameDialog";
|
||||
@@ -503,10 +503,7 @@ export function ChatView({ sessionId, onMenu, onClosePane }: { sessionId: string
|
||||
<ActivityLine sessionId={sessionId} draft={draft} pendingTool={pendingTool} waiting={waiting} permissionPending={permissions.length > 0 || questions.length > 0} />
|
||||
)}
|
||||
{openTodos.length > 0 && (busy || permissions.length > 0 || questions.length > 0) && (
|
||||
<div className="todos">
|
||||
<div style={{ color: "var(--muted)", fontSize: 12 }}>{t("tool.todos")}</div>
|
||||
<TodoList todos={todos} />
|
||||
</div>
|
||||
<TodoPanel todos={todos} />
|
||||
)}
|
||||
{permissions.map((p) => (
|
||||
<PermissionCard key={p.id} req={p} currentId={sessionId} />
|
||||
@@ -633,3 +630,24 @@ function PhaseBar({ phase, deep }: { phase?: string; deep: boolean }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Tasks of the model above the composer; folds to one line (current task, progress). */
|
||||
function TodoPanel({ todos }: { todos: Todo[] }) {
|
||||
const t = useT();
|
||||
const [collapsed, setCollapsed] = useTodosCollapsed();
|
||||
const done = todos.filter((x) => x.status === "completed").length;
|
||||
const current = todos.find((x) => x.status === "in_progress");
|
||||
return (
|
||||
<div className={`todos ${collapsed ? "collapsed" : ""}`}>
|
||||
<button className="todos-head" onClick={() => setCollapsed(!collapsed)} aria-expanded={!collapsed}>
|
||||
<Icon name={collapsed ? "chevron-right" : "chevron-down"} />
|
||||
<span>{t("tool.todos")}</span>
|
||||
<span className="todos-count">
|
||||
{done}/{todos.length}
|
||||
</span>
|
||||
{collapsed && current && <span className="todos-current">{current.content}</span>}
|
||||
</button>
|
||||
{!collapsed && <TodoList todos={todos} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -25,3 +25,21 @@ export function useReasoningMode(): ReasoningMode {
|
||||
getReasoningMode,
|
||||
);
|
||||
}
|
||||
|
||||
const TODOS_KEY = "codecraft.todosCollapsed";
|
||||
|
||||
/** Task list above the composer folded to a one-line summary (per device). */
|
||||
export function useTodosCollapsed(): [boolean, (v: boolean) => void] {
|
||||
const v = useSyncExternalStore(
|
||||
(l) => {
|
||||
listeners.add(l);
|
||||
return () => listeners.delete(l);
|
||||
},
|
||||
() => localStorage.getItem(TODOS_KEY) === "1",
|
||||
);
|
||||
const set = (c: boolean) => {
|
||||
localStorage.setItem(TODOS_KEY, c ? "1" : "0");
|
||||
listeners.forEach((l) => l());
|
||||
};
|
||||
return [v, set];
|
||||
}
|
||||
@@ -721,6 +721,39 @@ button.session-group:hover {
|
||||
margin: 8px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.todos-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--muted);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.todos-head:hover {
|
||||
color: var(--fg1);
|
||||
}
|
||||
.todos-count {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.todos-current {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.todos.collapsed {
|
||||
padding-block: 6px;
|
||||
}
|
||||
.todos ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
@@ -3773,3 +3806,104 @@ span.chat-tab > button {
|
||||
.instructions-modal {
|
||||
width: min(640px, calc(100vw - 32px));
|
||||
}
|
||||
|
||||
/* --- short screens ----------------------------------------------------------
|
||||
* Square phones (e.g. Clicks Communicator, ~393x400 CSS px with a hardware
|
||||
* keyboard) and phones in landscape: height is the scarce dimension, so bars,
|
||||
* the composer and the new-chat wizard get tighter and the chat keeps the room. */
|
||||
@media (max-height: 560px) {
|
||||
.topbar {
|
||||
min-height: 40px;
|
||||
padding: 4px 10px;
|
||||
padding-top: max(4px, env(safe-area-inset-top));
|
||||
gap: 4px;
|
||||
}
|
||||
.topbar button.ghost {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
.chat {
|
||||
padding: 10px 12px 14px;
|
||||
}
|
||||
.msg {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.composer-wrap {
|
||||
padding: 6px 10px;
|
||||
padding-bottom: max(6px, env(safe-area-inset-bottom));
|
||||
}
|
||||
.composer textarea {
|
||||
min-height: 38px;
|
||||
max-height: 30vh;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
/* One placeholder line instead of two (the textarea grows with its content). */
|
||||
.composer textarea::placeholder {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.composer-bar {
|
||||
margin-top: 4px;
|
||||
gap: 4px;
|
||||
}
|
||||
.composer-bar select {
|
||||
max-width: 150px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
.composer-actions button {
|
||||
padding: 4px 10px;
|
||||
}
|
||||
.todos {
|
||||
margin: 4px 0;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
.page {
|
||||
padding: 12px;
|
||||
}
|
||||
.wiz-title {
|
||||
font-size: 19px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.wiz-sub {
|
||||
display: none;
|
||||
}
|
||||
.wiz-panel {
|
||||
padding: 12px;
|
||||
}
|
||||
.wiz-step {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.wiz-step h3 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.wiz-cards,
|
||||
.wiz-cards.five {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
.wiz-card {
|
||||
padding: 8px 10px;
|
||||
gap: 8px;
|
||||
min-height: 44px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.wiz-card.active {
|
||||
padding: 7px 9px;
|
||||
}
|
||||
.wiz-card-text {
|
||||
padding-right: 14px;
|
||||
}
|
||||
.wiz-card-desc {
|
||||
display: none;
|
||||
}
|
||||
.wiz-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.wiz-num {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,10 @@
|
||||
* under "unreleased"; scripts/release.sh renames the key to the release version.
|
||||
*/
|
||||
export const WHATS_NEW: Record<string, { en: string[]; de: string[] }> = {
|
||||
unreleased: {
|
||||
en: ["The task list of the model can be folded (click on \"Tasks\")", "Better layout on square and short screens (e.g. Clicks Communicator, landscape)"],
|
||||
de: ["Die Aufgabenliste des Modells lässt sich zuklappen (Klick auf „Aufgaben“)", "Besseres Layout auf quadratischen und niedrigen Displays (z. B. Clicks Communicator, Querformat)"],
|
||||
},
|
||||
"202611-00010": {
|
||||
en: ["Chats with many images (screenshots, generated images) open much faster – images load when they scroll into view"],
|
||||
de: ["Chats mit vielen Bildern (Screenshots, generierte Bilder) öffnen viel schneller – Bilder laden erst beim Hinscrollen"],
|
||||
|
||||
Reference in new issue
Block a user