Skip to content

Commit ecc91f3

Browse files
oratisclaude
andauthored
fix(v0.1.2): tool casing, project folder picker, sessions, control locking (#70)
Audits and fixes from the 0.1.1 playtest screenshot: **Tool input field-name fix.** tool_write was failing with "missing required key filePath" because DeepSeek occasionally emits snake_case schema fields with camelCase keys in tool_use calls. All 6 Mac tool wrappers (Read/Write/Edit/Bash/Glob/Grep) now accept either case via a tolerant pickStr/pickNum/pickBool helper. **Project folder picker.** Before chat is usable, users now pick a project folder via the native file dialog. The path persists to ~/.deepcode/settings.json#projectPath and is threaded as `cwd` into every agent turn (was hard-coded `/`). Sidebar shows the active project + a ⇄ switch button to re-open the picker. **Session persistence.** Added Tauri commands session_create + session_append. mac-agent now creates a JSONL on first turn (id like 2026-05-28-<8hex>) and appends user + assistant messages. Sidebar refreshes via a sessionEpoch counter on turn_done, so freshly-started conversations appear in the Today bucket. **Mid-turn controls locked.** New controlsLocked flag computed from busy + pendingApproval; mode / model / effort dropdowns all respect it. Was previously freely switchable mid-turn, which contradicted whatever the agent loop already had baked in. **Inspector rail wired.** All 6 rail icons now route to their respective screens (Plan→Permissions, Sessions, Plugins, Skills, MCP, About, Settings, cog→Settings). Expand-chevron ‹ still deferred. **Proper dropdowns** for mode / model / effort — new Dropdown component renders a click-popover with inline descriptions + meta, replacing brittle click-to-cycle UX. 5 official modes now surfaced (default / acceptEdits / plan / dontAsk / bypassPermissions). **ChatScreen** folded into ReplScreen — the legacy split chat/terminal view isn't on the new shell. Kept as a stub for symmetry. Bundle dropped from 673 KB → 345 KB as a side effect (xterm out of the path). **System prompt** now informs the LLM of the working directory + that tool schemas are snake_case, so it stops guessing key casings. Version bumped to 0.1.2 across tauri.conf / both package.jsons / Cargo. CHANGELOG entry added. All typecheck + 533 tests pass. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0e8a499 commit ecc91f3

21 files changed

Lines changed: 948 additions & 167 deletions

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,38 @@ All notable changes to DeepCode are documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.1.2] — 2026-05-28
9+
10+
### Fixes — caught from user playtest of 0.1.1
11+
- **Tool input field-name fix.** `tool_write` (and read / edit / bash /
12+
glob / grep) were failing with `missing required key filePath` when
13+
DeepSeek emitted snake_case keys but the wrapper expected camelCase.
14+
All 6 Mac tool wrappers now accept either case via a tolerant
15+
`pickStr / pickNum / pickBool` helper.
16+
- **Project folder picker.** First launch now shows a "Pick a project
17+
folder" overlay before chat. The chosen path is persisted to
18+
`~/.deepcode/settings.json#projectPath` and threaded into every
19+
agent turn as `cwd`. Sidebar shows the active project + a ``
20+
switch button.
21+
- **Session persistence.** Each turn now writes a JSONL session under
22+
`~/.deepcode/sessions/<id>.jsonl`. Sidebar refreshes after every
23+
turn so newly-started sessions appear in the Today bucket.
24+
- **Mid-turn controls locked.** Mode / model / effort dropdowns disable
25+
while the agent is responding or awaiting approval (was previously
26+
freely switchable mid-turn).
27+
- **Inspector rail buttons work.** All 6 rail icons now route to
28+
their respective screens (Plan → Permissions, Sessions, Plugins,
29+
Skills, MCP, About, Settings). Expand-chevron ‹ still deferred.
30+
31+
### UX improvements
32+
- **Proper dropdowns** for mode / model / effort — click-popover with
33+
inline descriptions and meta annotations, replacing the brittle
34+
click-to-cycle pattern.
35+
- 5 official mode options surfaced (default / acceptEdits / plan /
36+
dontAsk / bypassPermissions) instead of 3.
37+
- ReplScreen carries projectPath through to the system prompt so the
38+
LLM knows where it's working.
39+
840
## [0.1.1] — 2026-05-28
941

1042
### Visual redesign — phase 1

apps/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "deepcode-cli",
3-
"version": "0.1.1",
3+
"version": "0.1.2",
44
"description": "DeepCode CLI — DeepSeek-powered AI coding agent, parity with Claude Code",
55
"license": "MIT",
66
"type": "module",

apps/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@deepcode/desktop",
3-
"version": "0.1.1",
3+
"version": "0.1.2",
44
"private": true,
55
"description": "DeepCode Mac desktop client — Tauri + React",
66
"license": "MIT",

apps/desktop/src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "deepcode_desktop"
3-
version = "0.1.1"
3+
version = "0.1.2"
44
description = "DeepCode Mac desktop client"
55
authors = ["oratis"]
66
edition = "2021"

apps/desktop/src-tauri/src/commands.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,81 @@ pub fn append_allow_matcher(matcher: String) -> Result<(), String> {
114114
settings::write_user(&value)
115115
}
116116

117+
/// Create a new session JSONL with a metadata header line. Returns the
118+
/// generated session id. The id format matches what @deepcode/core's
119+
/// SessionManager produces: `YYYY-MM-DD-<random>`.
120+
#[tauri::command]
121+
pub fn session_create(cwd: String) -> Result<String, String> {
122+
let Some(home) = dirs::home_dir() else {
123+
return Err("no home directory".into());
124+
};
125+
let now = std::time::SystemTime::now();
126+
let secs = now
127+
.duration_since(std::time::UNIX_EPOCH)
128+
.map_err(|e| e.to_string())?
129+
.as_secs();
130+
let date = format_date(secs);
131+
// Lightweight unique suffix from time-nanos — no extra crate dep
132+
let nanos = now
133+
.duration_since(std::time::UNIX_EPOCH)
134+
.map_err(|e| e.to_string())?
135+
.subsec_nanos();
136+
let rand_id = format!("{:08x}", nanos);
137+
let id = format!("{}-{}", date, rand_id);
138+
let dir = home.join(".deepcode").join("sessions");
139+
std::fs::create_dir_all(&dir).map_err(|e| format!("mkdir {}: {}", dir.display(), e))?;
140+
let path = dir.join(format!("{}.jsonl", id));
141+
let header = serde_json::json!({
142+
"type": "session_meta",
143+
"id": id,
144+
"cwd": cwd,
145+
"created_at": secs,
146+
"client": "desktop"
147+
});
148+
let line = format!("{}\n", header);
149+
std::fs::write(&path, line).map_err(|e| format!("write {}: {}", path.display(), e))?;
150+
Ok(id)
151+
}
152+
153+
/// Append a single JSON line to a session's JSONL file.
154+
#[tauri::command]
155+
pub fn session_append(id: String, message: serde_json::Value) -> Result<(), String> {
156+
let Some(home) = dirs::home_dir() else {
157+
return Err("no home directory".into());
158+
};
159+
let path = home
160+
.join(".deepcode")
161+
.join("sessions")
162+
.join(format!("{}.jsonl", id));
163+
let line = format!("{}\n", message);
164+
use std::io::Write;
165+
let mut f = std::fs::OpenOptions::new()
166+
.create(true)
167+
.append(true)
168+
.open(&path)
169+
.map_err(|e| format!("open {}: {}", path.display(), e))?;
170+
f.write_all(line.as_bytes())
171+
.map_err(|e| format!("write {}: {}", path.display(), e))
172+
}
173+
174+
fn format_date(secs: u64) -> String {
175+
// Simple YYYY-MM-DD; days since epoch math is enough for filename use.
176+
let days = secs / 86_400;
177+
// Reference: 1970-01-01 was a Thursday; we compute YMD via the
178+
// standard "civil_from_days" algorithm by Howard Hinnant.
179+
let z = days as i64 + 719_468;
180+
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
181+
let doe = (z - era * 146_097) as u64;
182+
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
183+
let y = yoe as i64 + era * 400;
184+
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
185+
let mp = (5 * doy + 2) / 153;
186+
let d = doy - (153 * mp + 2) / 5 + 1;
187+
let m = if mp < 10 { mp + 3 } else { mp - 9 };
188+
let y = if m <= 2 { y + 1 } else { y };
189+
format!("{:04}-{:02}-{:02}", y, m, d)
190+
}
191+
117192
/// List session files under ~/.deepcode/sessions/. Returns just metadata.
118193
#[derive(Serialize)]
119194
pub struct SessionMeta {

apps/desktop/src-tauri/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ mod tools;
1717
use commands::{
1818
append_allow_matcher, cli_path, get_app_info, get_settings_path, list_sessions,
1919
load_keybindings, load_settings_file, open_url, read_credentials, save_credentials,
20-
save_keybindings, save_settings_file,
20+
save_keybindings, save_settings_file, session_append, session_create,
2121
};
2222
use tools::{tool_bash, tool_edit, tool_glob, tool_grep, tool_read, tool_write};
2323
use tauri::Manager;
@@ -41,6 +41,8 @@ pub fn run() {
4141
append_allow_matcher,
4242
load_keybindings,
4343
save_keybindings,
44+
session_create,
45+
session_append,
4446
list_sessions,
4547
cli_path,
4648
open_url,

apps/desktop/src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "DeepCode",
4-
"version": "0.1.1",
4+
"version": "0.1.2",
55
"identifier": "dev.deepcode.desktop",
66
"build": {
77
"frontendDist": "../dist",

apps/desktop/src/App.tsx

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
// Top-level React component for desktop client.
22
// Spec: docs/VISUAL_DESIGN.html
3-
// Milestone: 0.1.1design-aligned 3-column shell
3+
// Milestone: 0.1.2adds project-folder flow + inspector wiring + session refresh.
44

55
import { useEffect, useState } from 'react';
66
import { InspectorRail } from './components/InspectorRail.js';
7+
import { ProjectPickerOverlay } from './components/ProjectPickerOverlay.js';
78
import { Sidebar } from './components/Sidebar.js';
89
import { UpdateBanner } from './components/UpdateBanner.js';
10+
import { clearHistory as clearAgentHistory } from './lib/mac-agent.js';
11+
import { loadProjectPath, saveProjectPath } from './lib/project.js';
912
import { onUpdateDownloaded, startUpdaterPolling } from './lib/updater.js';
1013
import { AboutScreen } from './screens/About.js';
11-
import { ChatScreen } from './screens/Chat.js';
1214
import { MCPManagerScreen } from './screens/MCPManager.js';
1315
import { OnboardingScreen } from './screens/Onboarding.js';
1416
import { PermissionsScreen } from './screens/Permissions.js';
@@ -22,12 +24,15 @@ import type { UpdateInfo } from './types/global.js';
2224

2325
export function App(): JSX.Element {
2426
const [hasKey, setHasKey] = useState<boolean | null>(null);
27+
const [projectPath, setProjectPath] = useState<string | null | undefined>(undefined);
2528
const [update, setUpdate] = useState<UpdateInfo | null>(null);
2629
const [screen, setScreen] = useState<ScreenName>('repl');
2730
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
31+
const [sessionEpoch, setSessionEpoch] = useState(0);
2832

2933
useEffect(() => {
3034
void window.deepcode.creds.load().then((c) => setHasKey(c.hasKey));
35+
void loadProjectPath().then((p) => setProjectPath(p ?? null));
3136
const offShim = window.deepcode.onUpdateDownloaded((info) => setUpdate(info));
3237
const offReal = onUpdateDownloaded((info) => setUpdate(info));
3338
startUpdaterPolling();
@@ -37,7 +42,13 @@ export function App(): JSX.Element {
3742
};
3843
}, []);
3944

40-
if (hasKey === null) {
45+
async function handlePickProject(path: string): Promise<void> {
46+
await saveProjectPath(path);
47+
setProjectPath(path);
48+
}
49+
50+
// Loading state
51+
if (hasKey === null || projectPath === undefined) {
4152
return (
4253
<div
4354
style={{
@@ -59,22 +70,40 @@ export function App(): JSX.Element {
5970
return <OnboardingScreen onComplete={() => setHasKey(true)} />;
6071
}
6172

73+
// No project picked yet → folder picker overlay
74+
if (!projectPath) {
75+
return <ProjectPickerOverlay onPicked={handlePickProject} />;
76+
}
77+
6278
// Main shell: 3-column grid.
6379
return (
6480
<div className="app-shell">
6581
{update && <UpdateBanner info={update} />}
6682
<Sidebar
83+
key={`sb-${sessionEpoch}`}
84+
projectPath={projectPath}
6785
activeSessionId={activeSessionId}
6886
onPickSession={(id) => {
6987
setActiveSessionId(id);
7088
setScreen('repl');
7189
}}
7290
onNewSession={() => {
91+
clearAgentHistory();
7392
setActiveSessionId(null);
7493
setScreen('repl');
94+
// Force ReplScreen to remount with a clean message history
95+
setSessionEpoch((k) => k + 1);
96+
}}
97+
onSwitchProject={async () => {
98+
// Force-show the picker again by clearing state.
99+
setProjectPath(null);
75100
}}
76101
/>
77-
<main className="chat-main">{renderScreen(screen, setScreen)}</main>
102+
<main className="chat-main" key={`main-${sessionEpoch}`}>
103+
{renderScreen(screen, setScreen, projectPath, () =>
104+
setSessionEpoch((k) => k + 1),
105+
)}
106+
</main>
78107
<InspectorRail
79108
activeScreen={screen}
80109
onChange={(s) => setScreen(s)}
@@ -87,10 +116,13 @@ export function App(): JSX.Element {
87116
function renderScreen(
88117
screen: ScreenName,
89118
setScreen: (s: ScreenName) => void,
119+
projectPath: string,
120+
onTurnComplete: () => void,
90121
): JSX.Element {
91122
switch (screen) {
92123
case 'chat':
93-
return <ChatScreen />;
124+
// 'chat' folded into 'repl' — the new shell has only the REPL surface.
125+
return <ReplScreen projectPath={projectPath} onTurnComplete={onTurnComplete} />;
94126
case 'sessions':
95127
return (
96128
<div className="legacy-screen">
@@ -138,6 +170,6 @@ function renderScreen(
138170
);
139171
case 'repl':
140172
default:
141-
return <ReplScreen />;
173+
return <ReplScreen projectPath={projectPath} onTurnComplete={onTurnComplete} />;
142174
}
143175
}

0 commit comments

Comments
 (0)