diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e2aad7..cf67aa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,38 @@ All notable changes to DeepCode are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.2] — 2026-05-28 + +### Fixes — caught from user playtest of 0.1.1 +- **Tool input field-name fix.** `tool_write` (and read / edit / bash / + glob / grep) were failing with `missing required key filePath` when + DeepSeek emitted snake_case keys but the wrapper expected camelCase. + All 6 Mac tool wrappers now accept either case via a tolerant + `pickStr / pickNum / pickBool` helper. +- **Project folder picker.** First launch now shows a "Pick a project + folder" overlay before chat. The chosen path is persisted to + `~/.deepcode/settings.json#projectPath` and threaded into every + agent turn as `cwd`. Sidebar shows the active project + a `⇄` + switch button. +- **Session persistence.** Each turn now writes a JSONL session under + `~/.deepcode/sessions/.jsonl`. Sidebar refreshes after every + turn so newly-started sessions appear in the Today bucket. +- **Mid-turn controls locked.** Mode / model / effort dropdowns disable + while the agent is responding or awaiting approval (was previously + freely switchable mid-turn). +- **Inspector rail buttons work.** All 6 rail icons now route to + their respective screens (Plan → Permissions, Sessions, Plugins, + Skills, MCP, About, Settings). Expand-chevron ‹ still deferred. + +### UX improvements +- **Proper dropdowns** for mode / model / effort — click-popover with + inline descriptions and meta annotations, replacing the brittle + click-to-cycle pattern. +- 5 official mode options surfaced (default / acceptEdits / plan / + dontAsk / bypassPermissions) instead of 3. +- ReplScreen carries projectPath through to the system prompt so the + LLM knows where it's working. + ## [0.1.1] — 2026-05-28 ### Visual redesign — phase 1 diff --git a/apps/cli/package.json b/apps/cli/package.json index 6e8851b..d58cf77 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "deepcode-cli", - "version": "0.1.1", + "version": "0.1.2", "description": "DeepCode CLI — DeepSeek-powered AI coding agent, parity with Claude Code", "license": "MIT", "type": "module", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 0863f6a..47d2f91 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@deepcode/desktop", - "version": "0.1.1", + "version": "0.1.2", "private": true, "description": "DeepCode Mac desktop client — Tauri + React", "license": "MIT", diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index ef83151..4198971 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -672,7 +672,7 @@ dependencies = [ [[package]] name = "deepcode_desktop" -version = "0.1.0" +version = "0.1.1" dependencies = [ "dirs 5.0.1", "serde", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index f1ccd9e..21adc80 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deepcode_desktop" -version = "0.1.1" +version = "0.1.2" description = "DeepCode Mac desktop client" authors = ["oratis"] edition = "2021" diff --git a/apps/desktop/src-tauri/src/commands.rs b/apps/desktop/src-tauri/src/commands.rs index f26b976..8d99697 100644 --- a/apps/desktop/src-tauri/src/commands.rs +++ b/apps/desktop/src-tauri/src/commands.rs @@ -114,6 +114,81 @@ pub fn append_allow_matcher(matcher: String) -> Result<(), String> { settings::write_user(&value) } +/// Create a new session JSONL with a metadata header line. Returns the +/// generated session id. The id format matches what @deepcode/core's +/// SessionManager produces: `YYYY-MM-DD-`. +#[tauri::command] +pub fn session_create(cwd: String) -> Result { + let Some(home) = dirs::home_dir() else { + return Err("no home directory".into()); + }; + let now = std::time::SystemTime::now(); + let secs = now + .duration_since(std::time::UNIX_EPOCH) + .map_err(|e| e.to_string())? + .as_secs(); + let date = format_date(secs); + // Lightweight unique suffix from time-nanos — no extra crate dep + let nanos = now + .duration_since(std::time::UNIX_EPOCH) + .map_err(|e| e.to_string())? + .subsec_nanos(); + let rand_id = format!("{:08x}", nanos); + let id = format!("{}-{}", date, rand_id); + let dir = home.join(".deepcode").join("sessions"); + std::fs::create_dir_all(&dir).map_err(|e| format!("mkdir {}: {}", dir.display(), e))?; + let path = dir.join(format!("{}.jsonl", id)); + let header = serde_json::json!({ + "type": "session_meta", + "id": id, + "cwd": cwd, + "created_at": secs, + "client": "desktop" + }); + let line = format!("{}\n", header); + std::fs::write(&path, line).map_err(|e| format!("write {}: {}", path.display(), e))?; + Ok(id) +} + +/// Append a single JSON line to a session's JSONL file. +#[tauri::command] +pub fn session_append(id: String, message: serde_json::Value) -> Result<(), String> { + let Some(home) = dirs::home_dir() else { + return Err("no home directory".into()); + }; + let path = home + .join(".deepcode") + .join("sessions") + .join(format!("{}.jsonl", id)); + let line = format!("{}\n", message); + use std::io::Write; + let mut f = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&path) + .map_err(|e| format!("open {}: {}", path.display(), e))?; + f.write_all(line.as_bytes()) + .map_err(|e| format!("write {}: {}", path.display(), e)) +} + +fn format_date(secs: u64) -> String { + // Simple YYYY-MM-DD; days since epoch math is enough for filename use. + let days = secs / 86_400; + // Reference: 1970-01-01 was a Thursday; we compute YMD via the + // standard "civil_from_days" algorithm by Howard Hinnant. + let z = days as i64 + 719_468; + let era = if z >= 0 { z } else { z - 146_096 } / 146_097; + let doe = (z - era * 146_097) as u64; + let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; + let y = yoe as i64 + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let d = doy - (153 * mp + 2) / 5 + 1; + let m = if mp < 10 { mp + 3 } else { mp - 9 }; + let y = if m <= 2 { y + 1 } else { y }; + format!("{:04}-{:02}-{:02}", y, m, d) +} + /// List session files under ~/.deepcode/sessions/. Returns just metadata. #[derive(Serialize)] pub struct SessionMeta { diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 778ca3a..2d80747 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -17,7 +17,7 @@ mod tools; use commands::{ append_allow_matcher, cli_path, get_app_info, get_settings_path, list_sessions, load_keybindings, load_settings_file, open_url, read_credentials, save_credentials, - save_keybindings, save_settings_file, + save_keybindings, save_settings_file, session_append, session_create, }; use tools::{tool_bash, tool_edit, tool_glob, tool_grep, tool_read, tool_write}; use tauri::Manager; @@ -41,6 +41,8 @@ pub fn run() { append_allow_matcher, load_keybindings, save_keybindings, + session_create, + session_append, list_sessions, cli_path, open_url, diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 223ca7f..dffa658 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "DeepCode", - "version": "0.1.1", + "version": "0.1.2", "identifier": "dev.deepcode.desktop", "build": { "frontendDist": "../dist", diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 419e937..3012c99 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -1,14 +1,16 @@ // Top-level React component for desktop client. // Spec: docs/VISUAL_DESIGN.html -// Milestone: 0.1.1 — design-aligned 3-column shell +// Milestone: 0.1.2 — adds project-folder flow + inspector wiring + session refresh. import { useEffect, useState } from 'react'; import { InspectorRail } from './components/InspectorRail.js'; +import { ProjectPickerOverlay } from './components/ProjectPickerOverlay.js'; import { Sidebar } from './components/Sidebar.js'; import { UpdateBanner } from './components/UpdateBanner.js'; +import { clearHistory as clearAgentHistory } from './lib/mac-agent.js'; +import { loadProjectPath, saveProjectPath } from './lib/project.js'; import { onUpdateDownloaded, startUpdaterPolling } from './lib/updater.js'; import { AboutScreen } from './screens/About.js'; -import { ChatScreen } from './screens/Chat.js'; import { MCPManagerScreen } from './screens/MCPManager.js'; import { OnboardingScreen } from './screens/Onboarding.js'; import { PermissionsScreen } from './screens/Permissions.js'; @@ -22,12 +24,15 @@ import type { UpdateInfo } from './types/global.js'; export function App(): JSX.Element { const [hasKey, setHasKey] = useState(null); + const [projectPath, setProjectPath] = useState(undefined); const [update, setUpdate] = useState(null); const [screen, setScreen] = useState('repl'); const [activeSessionId, setActiveSessionId] = useState(null); + const [sessionEpoch, setSessionEpoch] = useState(0); useEffect(() => { void window.deepcode.creds.load().then((c) => setHasKey(c.hasKey)); + void loadProjectPath().then((p) => setProjectPath(p ?? null)); const offShim = window.deepcode.onUpdateDownloaded((info) => setUpdate(info)); const offReal = onUpdateDownloaded((info) => setUpdate(info)); startUpdaterPolling(); @@ -37,7 +42,13 @@ export function App(): JSX.Element { }; }, []); - if (hasKey === null) { + async function handlePickProject(path: string): Promise { + await saveProjectPath(path); + setProjectPath(path); + } + + // Loading state + if (hasKey === null || projectPath === undefined) { return (
setHasKey(true)} />; } + // No project picked yet → folder picker overlay + if (!projectPath) { + return ; + } + // Main shell: 3-column grid. return (
{update && } { setActiveSessionId(id); setScreen('repl'); }} onNewSession={() => { + clearAgentHistory(); setActiveSessionId(null); setScreen('repl'); + // Force ReplScreen to remount with a clean message history + setSessionEpoch((k) => k + 1); + }} + onSwitchProject={async () => { + // Force-show the picker again by clearing state. + setProjectPath(null); }} /> -
{renderScreen(screen, setScreen)}
+
+ {renderScreen(screen, setScreen, projectPath, () => + setSessionEpoch((k) => k + 1), + )} +
setScreen(s)} @@ -87,10 +116,13 @@ export function App(): JSX.Element { function renderScreen( screen: ScreenName, setScreen: (s: ScreenName) => void, + projectPath: string, + onTurnComplete: () => void, ): JSX.Element { switch (screen) { case 'chat': - return ; + // 'chat' folded into 'repl' — the new shell has only the REPL surface. + return ; case 'sessions': return (
@@ -138,6 +170,6 @@ function renderScreen( ); case 'repl': default: - return ; + return ; } } diff --git a/apps/desktop/src/components/Dropdown.tsx b/apps/desktop/src/components/Dropdown.tsx new file mode 100644 index 0000000..0ceecda --- /dev/null +++ b/apps/desktop/src/components/Dropdown.tsx @@ -0,0 +1,177 @@ +// Lightweight click-popover dropdown. Matches the design's model-picker +// style (rounded pill + caret) so it can stand in for native + value={model} + onChange={setModel} + disabled={controlsLocked} + dot + title="DeepSeek model" + panelWidth={280} + options={MODEL_OPTIONS} + renderTrigger={(opt) => ( + <> + {opt.label} + {opt.meta} + + )} + /> + + value={effort} - onChange={(e) => void handleEffortChange(e.target.value as Effort)} - disabled={busy} - title="Effort tier — maxTokens + temperature" - style={{ - background: 'var(--bg-2)', - border: '1px solid var(--line-soft)', - borderRadius: 'var(--radius-sm)', - color: 'var(--text-1)', - padding: '5px 8px', - fontSize: 12, - }} - > - {EFFORTS.map((t) => ( - - ))} - + onChange={(v) => void handleEffortChange(v)} + disabled={controlsLocked} + title="Effort — maxTokens + temperature" + panelWidth={280} + options={EFFORT_OPTIONS} + renderTrigger={(opt) => ( + <> + {opt.label} + {opt.meta} + + )} + /> + {busy ? (