Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92a99c6a65 | ||
|
|
4c7d67cabf |
No files matched your search
@@ -3,6 +3,13 @@
|
|||||||
All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/);
|
All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/);
|
||||||
until 1.0, minor versions may contain breaking changes.
|
until 1.0, minor versions may contain breaking changes.
|
||||||
|
|
||||||
|
## [v202611-00030] – 2026-11-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **`/clear`**: the model forgets the conversation so far; the chat keeps showing it, a divider marks
|
||||||
|
the cut. Also starts a fresh session with Claude Code.
|
||||||
|
|
||||||
## [v202611-00029] – 2026-11-15
|
## [v202611-00029] – 2026-11-15
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="Version v202611-00029" src="https://img.shields.io/badge/version-v202611--00029-d69e2e?style=flat-square">
|
<img alt="Version v202611-00030" src="https://img.shields.io/badge/version-v202611--00030-d69e2e?style=flat-square">
|
||||||
<img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-689d6a?style=flat-square">
|
<img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-689d6a?style=flat-square">
|
||||||
<img alt="Rust backend" src="https://img.shields.io/badge/backend-Rust-b7410e?style=flat-square&logo=rust">
|
<img alt="Rust backend" src="https://img.shields.io/badge/backend-Rust-b7410e?style=flat-square&logo=rust">
|
||||||
<img alt="React PWA" src="https://img.shields.io/badge/frontend-React%20PWA-2d81ff?style=flat-square&logo=react">
|
<img alt="React PWA" src="https://img.shields.io/badge/frontend-React%20PWA-2d81ff?style=flat-square&logo=react">
|
||||||
|
|||||||
@@ -1128,6 +1128,13 @@ pub(crate) fn send_prompt(app: &AppState, session: crate::types::Session, text:
|
|||||||
tokio::spawn(do_compact(app.clone(), id.clone()));
|
tokio::spawn(do_compact(app.clone(), id.clone()));
|
||||||
return Ok(json!({"ok": true, "command": "compact"}));
|
return Ok(json!({"ok": true, "command": "compact"}));
|
||||||
}
|
}
|
||||||
|
if text == "/clear" {
|
||||||
|
if app.is_busy(&id) {
|
||||||
|
return Err(ApiError(StatusCode::CONFLICT, "session is busy".into()));
|
||||||
|
}
|
||||||
|
runner::clear_context(app, &id).map_err(|e| ApiError(StatusCode::INTERNAL_SERVER_ERROR, format!("{e:#}")))?;
|
||||||
|
return Ok(json!({"ok": true, "command": "clear"}));
|
||||||
|
}
|
||||||
let cwd = PathBuf::from(&session.cwd);
|
let cwd = PathBuf::from(&session.cwd);
|
||||||
let root = app.root_for(session.owner_id.as_deref(), &cwd);
|
let root = app.root_for(session.owner_id.as_deref(), &cwd);
|
||||||
let mut prompt_text = text.clone();
|
let mut prompt_text = text.clone();
|
||||||
|
|||||||
@@ -1723,10 +1723,43 @@ async fn compact_inner(app: Arc<App>, session_id: &str, cancel: CancellationToke
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Text of the marker `/clear` puts into the history (the chat view recognises it).
|
||||||
|
pub const CLEARED_MARKER: &str = "[Context cleared by the user. Earlier messages are no longer part of this conversation – start fresh and do not refer to them.]";
|
||||||
|
|
||||||
|
/// `/clear`: the model forgets the conversation so far (the chat keeps showing it). Like a
|
||||||
|
/// compaction without a summary: history before the marker is not sent any more.
|
||||||
|
pub fn clear_context(app: &App, session_id: &str) -> Result<()> {
|
||||||
|
save_user_parts(app, session_id, vec![Part::Summary { text: CLEARED_MARKER.into(), auto: false, tokens: None }])?;
|
||||||
|
app.update_session(session_id, |s| {
|
||||||
|
s.last_usage = None;
|
||||||
|
s.todos = Value::Null;
|
||||||
|
})?;
|
||||||
|
app.with_runtime(session_id, |r| r.read_files.clear());
|
||||||
|
// Claude Code continues its own conversation: start a new one.
|
||||||
|
if let Some(owner) = app.session(session_id).ok().and_then(|s| s.owner_id) {
|
||||||
|
let _ = app.store.set_user_setting(&owner, &format!("claude_sid:{session_id}"), &Option::<String>::None);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cleared_marker_starts_a_new_history() {
|
||||||
|
let mk = |role, parts| Message::new(role, parts);
|
||||||
|
let all = vec![
|
||||||
|
mk(Role::User, vec![Part::Text { text: "old question".into() }]),
|
||||||
|
mk(Role::Assistant, vec![Part::Text { text: "old answer".into() }]),
|
||||||
|
mk(Role::User, vec![Part::Summary { text: CLEARED_MARKER.into(), auto: false, tokens: None }]),
|
||||||
|
mk(Role::User, vec![Part::Text { text: "new question".into() }]),
|
||||||
|
];
|
||||||
|
let active = active_history(all);
|
||||||
|
assert_eq!(active.len(), 2);
|
||||||
|
assert!(!active.iter().any(|m| message_text(m).contains("old")));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fit_keeps_head_and_tail_within_budget() {
|
fn fit_keeps_head_and_tail_within_budget() {
|
||||||
let text = format!("{}{}{}", "A".repeat(5000), "M".repeat(5000), "Z".repeat(5000));
|
let text = format!("{}{}{}", "A".repeat(5000), "M".repeat(5000), "Z".repeat(5000));
|
||||||
|
|||||||
@@ -459,13 +459,15 @@ const MessageItem = memo(function MessageItem({ m, canEdit, onRewind, highlight
|
|||||||
return (
|
return (
|
||||||
<details className="summary-divider" data-mid={m.id} onToggle={(e) => setSummaryOpen((e.currentTarget as HTMLDetailsElement).open)}>
|
<details className="summary-divider" data-mid={m.id} onToggle={(e) => setSummaryOpen((e.currentTarget as HTMLDetailsElement).open)}>
|
||||||
<summary>
|
<summary>
|
||||||
{summary.auto
|
{summary.text.startsWith("[Context cleared")
|
||||||
|
? t("chat.cleared")
|
||||||
|
: summary.auto
|
||||||
? t("chat.compactedAuto", { tokens: summary.tokens ? fmtCompact(summary.tokens) : "?" })
|
? t("chat.compactedAuto", { tokens: summary.tokens ? fmtCompact(summary.tokens) : "?" })
|
||||||
: summary.tokens
|
: summary.tokens
|
||||||
? t("chat.compactedManual", { tokens: fmtCompact(summary.tokens) })
|
? t("chat.compactedManual", { tokens: fmtCompact(summary.tokens) })
|
||||||
: t("chat.compacted")}
|
: t("chat.compacted")}
|
||||||
</summary>
|
</summary>
|
||||||
{summaryOpen && <Markdown text={summary.text} />}
|
{summaryOpen && !summary.text.startsWith("[Context cleared") && <Markdown text={summary.text} />}
|
||||||
</details>
|
</details>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface Suggestion {
|
|||||||
|
|
||||||
const BUILTIN: { name: string; key: Key }[] = [
|
const BUILTIN: { name: string; key: Key }[] = [
|
||||||
{ name: "compact", key: "command.compact" },
|
{ name: "compact", key: "command.compact" },
|
||||||
|
{ name: "clear", key: "command.clear" },
|
||||||
{ name: "new", key: "command.new" },
|
{ name: "new", key: "command.new" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1144,4 +1144,6 @@ export const de: Messages = {
|
|||||||
"stats.table.agent": "nach Modus",
|
"stats.table.agent": "nach Modus",
|
||||||
"stats.table.user": "nach Benutzer",
|
"stats.table.user": "nach Benutzer",
|
||||||
"stats.table.session": "teuerste Chats",
|
"stats.table.session": "teuerste Chats",
|
||||||
|
"command.clear": "Bisheriges Gespräch vergessen (der Chat bleibt sichtbar)",
|
||||||
|
"chat.cleared": "Kontext geleert – das Modell sieht die Nachrichten darüber nicht mehr",
|
||||||
};
|
};
|
||||||
@@ -1143,6 +1143,8 @@ export const en = {
|
|||||||
"stats.table.agent": "by agent",
|
"stats.table.agent": "by agent",
|
||||||
"stats.table.user": "by user",
|
"stats.table.user": "by user",
|
||||||
"stats.table.session": "top chats",
|
"stats.table.session": "top chats",
|
||||||
|
"command.clear": "Forget the conversation so far (the chat stays visible)",
|
||||||
|
"chat.cleared": "Context cleared – the model no longer sees the messages above",
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Messages = Record<keyof typeof en, string>;
|
export type Messages = Record<keyof typeof en, string>;
|
||||||
@@ -3,6 +3,10 @@
|
|||||||
* under "unreleased"; scripts/release.sh renames the key to the release version.
|
* under "unreleased"; scripts/release.sh renames the key to the release version.
|
||||||
*/
|
*/
|
||||||
export const WHATS_NEW: Record<string, { en: string[]; de: string[] }> = {
|
export const WHATS_NEW: Record<string, { en: string[]; de: string[] }> = {
|
||||||
|
"202611-00030": {
|
||||||
|
en: ["/clear: the model forgets the conversation so far, the chat stays visible"],
|
||||||
|
de: ["/clear: das Modell vergisst das bisherige Gespräch, der Chat bleibt sichtbar"],
|
||||||
|
},
|
||||||
"202611-00029": {
|
"202611-00029": {
|
||||||
en: [
|
en: [
|
||||||
"Statistics: foldable sections, years of history with a year filter, export as XLSX/CSV",
|
"Statistics: foldable sections, years of history with a year filter, export as XLSX/CSV",
|
||||||
|
|||||||
Reference in new issue
Block a user