Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<id>.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
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
75 changes: 75 additions & 0 deletions apps/desktop/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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-<random>`.
#[tauri::command]
pub fn session_create(cwd: String) -> Result<String, String> {
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 {
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -41,6 +41,8 @@ pub fn run() {
append_allow_matcher,
load_keybindings,
save_keybindings,
session_create,
session_append,
list_sessions,
cli_path,
open_url,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
44 changes: 38 additions & 6 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
// Top-level React component for desktop client.
// Spec: docs/VISUAL_DESIGN.html
// Milestone: 0.1.1design-aligned 3-column shell
// Milestone: 0.1.2adds 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';
Expand All @@ -22,12 +24,15 @@ import type { UpdateInfo } from './types/global.js';

export function App(): JSX.Element {
const [hasKey, setHasKey] = useState<boolean | null>(null);
const [projectPath, setProjectPath] = useState<string | null | undefined>(undefined);
const [update, setUpdate] = useState<UpdateInfo | null>(null);
const [screen, setScreen] = useState<ScreenName>('repl');
const [activeSessionId, setActiveSessionId] = useState<string | null>(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();
Expand All @@ -37,7 +42,13 @@ export function App(): JSX.Element {
};
}, []);

if (hasKey === null) {
async function handlePickProject(path: string): Promise<void> {
await saveProjectPath(path);
setProjectPath(path);
}

// Loading state
if (hasKey === null || projectPath === undefined) {
return (
<div
style={{
Expand All @@ -59,22 +70,40 @@ export function App(): JSX.Element {
return <OnboardingScreen onComplete={() => setHasKey(true)} />;
}

// No project picked yet → folder picker overlay
if (!projectPath) {
return <ProjectPickerOverlay onPicked={handlePickProject} />;
}

// Main shell: 3-column grid.
return (
<div className="app-shell">
{update && <UpdateBanner info={update} />}
<Sidebar
key={`sb-${sessionEpoch}`}
projectPath={projectPath}
activeSessionId={activeSessionId}
onPickSession={(id) => {
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);
}}
/>
<main className="chat-main">{renderScreen(screen, setScreen)}</main>
<main className="chat-main" key={`main-${sessionEpoch}`}>
{renderScreen(screen, setScreen, projectPath, () =>
setSessionEpoch((k) => k + 1),
)}
</main>
<InspectorRail
activeScreen={screen}
onChange={(s) => setScreen(s)}
Expand All @@ -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 <ChatScreen />;
// 'chat' folded into 'repl' — the new shell has only the REPL surface.
return <ReplScreen projectPath={projectPath} onTurnComplete={onTurnComplete} />;
case 'sessions':
return (
<div className="legacy-screen">
Expand Down Expand Up @@ -138,6 +170,6 @@ function renderScreen(
);
case 'repl':
default:
return <ReplScreen />;
return <ReplScreen projectPath={projectPath} onTurnComplete={onTurnComplete} />;
}
}
Loading
Loading