Skip to content

Commit 0e0bc3d

Browse files
Ashish-dwi99claude
andcommitted
Workspace = projects, project = folders
Workspaces no longer require a root folder; they're a pure collection of projects. Folders attach to projects via scope rules and a project can hold one or many. Also surface Claude auto-memory .md files in the Memory view. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 3eebf8f commit 0e0bc3d

5 files changed

Lines changed: 255 additions & 150 deletions

File tree

dhee/ui/server.py

Lines changed: 99 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,96 @@ def _engram_from_memory(mem: Dict[str, Any]) -> Dict[str, Any]:
326326
}
327327

328328

329+
_AUTO_MEMORY_TIER_BY_TYPE = {
330+
"user": "high",
331+
"feedback": "high",
332+
"project": "medium",
333+
"reference": "medium",
334+
}
335+
336+
337+
def _parse_auto_memory_file(path: Path) -> Optional[Dict[str, Any]]:
338+
try:
339+
text = path.read_text(encoding="utf-8")
340+
except OSError:
341+
return None
342+
name = path.stem
343+
description = ""
344+
mem_type = "project"
345+
body = text
346+
if text.startswith("---"):
347+
end = text.find("\n---", 3)
348+
if end != -1:
349+
header = text[3:end]
350+
body = text[end + 4 :].lstrip("\n")
351+
for line in header.splitlines():
352+
key, sep, value = line.partition(":")
353+
if not sep:
354+
continue
355+
key = key.strip().lower()
356+
value = value.strip().strip('"').strip("'")
357+
if key == "name" and value:
358+
name = value
359+
elif key == "description" and value:
360+
description = value
361+
elif key == "type" and value:
362+
mem_type = value
363+
body = body.strip()
364+
if not body and not description:
365+
return None
366+
content_parts: List[str] = [f"[{mem_type}] {name}"]
367+
if description:
368+
content_parts.append(description)
369+
if body:
370+
content_parts.append(body)
371+
content = "\n\n".join(content_parts)
372+
try:
373+
created = datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc).strftime("%Y-%m-%d")
374+
except OSError:
375+
created = time.strftime("%Y-%m-%d")
376+
tier = _AUTO_MEMORY_TIER_BY_TYPE.get(mem_type, "medium")
377+
file_id = hashlib.sha1(str(path).encode("utf-8")).hexdigest()[:16]
378+
return {
379+
"id": f"auto:{file_id}",
380+
"tier": tier,
381+
"content": content,
382+
"source": "claude_auto_memory",
383+
"created": created,
384+
"tags": [mem_type, "auto-memory"],
385+
"decay": 1.0,
386+
"reaffirmed": 0,
387+
"tokens": _estimate_tokens(content),
388+
}
389+
390+
391+
def _auto_memory_engrams() -> List[Dict[str, Any]]:
392+
roots = [Path.home() / ".claude" / "projects"]
393+
engrams: List[Dict[str, Any]] = []
394+
for root in roots:
395+
if not root.is_dir():
396+
continue
397+
try:
398+
project_dirs = [p for p in root.iterdir() if p.is_dir()]
399+
except OSError:
400+
continue
401+
for project_dir in project_dirs:
402+
memory_dir = project_dir / "memory"
403+
if not memory_dir.is_dir():
404+
continue
405+
try:
406+
files = [p for p in memory_dir.iterdir() if p.is_file() and p.suffix == ".md"]
407+
except OSError:
408+
continue
409+
for path in files:
410+
if path.name.upper() == "MEMORY.MD":
411+
continue
412+
eng = _parse_auto_memory_file(path)
413+
if eng:
414+
engrams.append(eng)
415+
engrams.sort(key=lambda e: e.get("created") or "", reverse=True)
416+
return engrams
417+
418+
329419
# ─── App factory ──────────────────────────────────────────────────────────────
330420

331421

@@ -368,6 +458,7 @@ def list_memories(limit: int = 500) -> Dict[str, Any]:
368458
if isinstance(raw, dict):
369459
raw = raw.get("results") or raw.get("memories") or []
370460
engrams = [_engram_from_memory(m) for m in raw if m]
461+
engrams.extend(_auto_memory_engrams())
371462
return {"live": True, "engrams": engrams, "count": len(engrams)}
372463
except Exception as exc: # noqa: BLE001
373464
log.warning("list_memories failed: %s", exc)
@@ -637,43 +728,22 @@ def list_workspaces_api() -> Dict[str, Any]:
637728
def create_workspace_api(payload: WorkspaceRootCreatePayload) -> Dict[str, Any]:
638729
try:
639730
db = _get_db()
640-
root_path = _abs_user_path(payload.root_path or _ui_repo())
641-
workspace_name = _display_name(payload.name, fallback_path=root_path)
731+
workspace_name = _display_name(payload.name, fallback="Workspace")
732+
if not workspace_name:
733+
raise HTTPException(status_code=400, detail="Workspace name is required")
642734
workspace = db.upsert_workspace(
643735
{
644736
"user_id": _ui_user_id(),
645737
"name": workspace_name,
646738
"description": payload.description,
647-
"root_path": root_path,
739+
"root_path": None,
648740
"metadata": {"created_via": "sankhya-ui"},
649741
}
650742
)
651-
db.upsert_workspace_mount(
652-
{
653-
"workspace_id": workspace["id"],
654-
"user_id": _ui_user_id(),
655-
"mount_path": root_path,
656-
"label": os.path.basename(root_path.rstrip(os.sep)) or root_path,
657-
"is_primary": True,
658-
}
659-
)
660-
general = db.upsert_workspace_project(
661-
{
662-
"workspace_id": workspace["id"],
663-
"user_id": _ui_user_id(),
664-
"name": "General",
665-
"description": f"Default project for {workspace_name}",
666-
"default_runtime": "codex",
667-
"metadata": {"created_via": "sankhya-ui", "auto_created": True},
668-
}
669-
)
670-
db.replace_workspace_project_scope_rules(
671-
project_id=str(general.get("id") or ""),
672-
user_id=_ui_user_id(),
673-
rules=[{"path_prefix": root_path, "label": "root"}],
674-
)
675-
_mirror_runtime_sessions(db, extra_paths=[root_path])
743+
_mirror_runtime_sessions(db)
676744
return {"ok": True, "workspace": _workspace_summary(db, workspace)}
745+
except HTTPException:
746+
raise
677747
except Exception as exc: # noqa: BLE001
678748
raise HTTPException(status_code=400, detail=str(exc)) from exc
679749

@@ -819,19 +889,14 @@ def create_workspace_project_api(
819889
}
820890
)
821891
rules = _normalize_scope_rules(payload.scope_rules)
822-
if not rules:
823-
rules = [{"path_prefix": _workspace_primary_path(workspace), "label": "root"}]
824892
db.replace_workspace_project_scope_rules(
825893
project_id=str(project.get("id") or ""),
826894
user_id=_ui_user_id(),
827895
rules=rules,
828896
)
829897
_mirror_runtime_sessions(
830898
db,
831-
extra_paths=[
832-
_workspace_primary_path(workspace),
833-
*[str(rule.get("path_prefix") or "") for rule in rules],
834-
],
899+
extra_paths=[str(rule.get("path_prefix") or "") for rule in rules],
835900
)
836901
return {"ok": True, "project": _project_summary(db, project)}
837902
except HTTPException:

dhee/ui/web/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,8 @@ export default function App() {
190190
}
191191
}, [view, activeTaskId, selectedProjectId, selectedWorkspaceId, selectedSessionId]);
192192

193-
const handleCreateWorkspace = async (name: string, workspacePath: string) => {
194-
await api.createWorkspaceRoot(name, workspacePath);
193+
const handleCreateWorkspace = async (name: string) => {
194+
await api.createWorkspaceRoot(name);
195195
await refreshProjects();
196196
await refreshWorkspaceGraph();
197197
};

dhee/ui/web/src/api.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,10 @@ export const api = {
130130
),
131131
projects: () => j<ProjectIndexSnapshot>("/workspaces"),
132132
workspaces: () => j<ProjectIndexSnapshot>("/workspaces"),
133-
createWorkspaceRoot: (name: string, root_path: string, description?: string) =>
133+
createWorkspaceRoot: (name: string, description?: string) =>
134134
j<{ ok: boolean; workspace: WorkspaceDetailSnapshot["workspace"] }>("/workspaces", {
135135
method: "POST",
136-
body: JSON.stringify({ name, root_path, description }),
136+
body: JSON.stringify({ name, description }),
137137
}),
138138
createProject: (workspaceId: string, payload: { name: string; description?: string; default_runtime?: string; color?: string; icon?: string; scope_rules?: { path_prefix: string; label?: string }[] }) =>
139139
j<{ ok: boolean; project: ProjectSummary }>(
@@ -179,7 +179,7 @@ export const api = {
179179
),
180180
updateWorkspace: (
181181
workspaceId: string,
182-
payload: { label?: string; description?: string; root_path?: string },
182+
payload: { label?: string; description?: string },
183183
) =>
184184
j<{ ok: boolean; workspace: WorkspaceDetailSnapshot["workspace"] }>(
185185
`/workspaces/${encodeURIComponent(workspaceId)}`,

0 commit comments

Comments
 (0)