Implement Docker Web project management on top of workspace_id while keeping host mode workspace behavior intact.
Backend changes:
- Move Docker user files to users/<user>/projects/<project_id>/{project,data,logs} and shared user state to users/<user>/personal.
- Add automatic legacy migration from old project/data/logs/agentskills layouts and from the temporary project/shared layout.
- Generate project IDs server-side from the project name, reserve internal IDs, and hide project paths from the UI.
- Add project rename/delete APIs; deleting a Docker project removes its project directory and releases terminal/container state.
- Add host workspace rename/delete APIs; deleting a host workspace only removes catalog entries and keeps files on disk.
- Scope Docker containers, terminals, uploads, conversations, storage, and running task metadata by workspace_id.
- Store private skills under users/<user>/personal/agentskills and sync them into each project workspace.
- Inject the current project/workspace name through prompts/workspace_system.txt.
Frontend changes:
- Replace Docker file-tree area with project management UX and reuse host multi-workspace running-task behavior.
- Add a management dialog for creating, renaming, and deleting projects/workspaces.
- Show project labels instead of internal IDs in toasts and project lists.
- Clear stale conversation history during project/workspace switching and show loading until the new list is ready.
- Keep running-task loader/check behavior consistent across host workspaces and Docker projects.
Validation:
- python3 -m py_compile modules/user_manager.py modules/host_workspace_manager.py modules/skills_manager.py core/main_terminal_parts/tools_execution.py server/auth.py server/context.py server/status.py server/conversation.py server/tasks.py utils/context_manager.py
- python3 -m unittest test.test_server_refactor_smoke
- npm run build --silent 2>&1 | tail -n 20
221 lines
5.6 KiB
Vue
221 lines
5.6 KiB
Vue
<template>
|
||
<div
|
||
class="host-workspace-dialog-overlay"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
:aria-label="workspaceKind === 'project' ? '新建项目' : '新建工作区'"
|
||
@click.self="handleOverlayClose"
|
||
>
|
||
<form class="host-workspace-dialog" @submit.prevent="$emit('submit')">
|
||
<div class="host-workspace-dialog__header">
|
||
<div class="host-workspace-dialog__title">
|
||
{{ workspaceKind === 'project' ? '新建项目' : '新建工作区' }}
|
||
</div>
|
||
<button
|
||
type="button"
|
||
class="host-workspace-dialog__close"
|
||
:disabled="submitting"
|
||
@click="$emit('close')"
|
||
>
|
||
关闭
|
||
</button>
|
||
</div>
|
||
|
||
<div class="host-workspace-dialog__body">
|
||
<label v-if="workspaceKind !== 'project'" class="host-workspace-dialog__field">
|
||
<span class="host-workspace-dialog__label">
|
||
工作区路径
|
||
</span>
|
||
<input
|
||
:value="pathValue"
|
||
type="text"
|
||
class="host-workspace-dialog__input"
|
||
placeholder="请输入绝对路径或相对仓库路径"
|
||
autocomplete="off"
|
||
:disabled="submitting"
|
||
@input="$emit('update:pathValue', ($event.target as HTMLInputElement).value)"
|
||
/>
|
||
</label>
|
||
|
||
<label class="host-workspace-dialog__field">
|
||
<span class="host-workspace-dialog__label">
|
||
{{ workspaceKind === 'project' ? '项目名称' : '工作区名称(可选)' }}
|
||
</span>
|
||
<input
|
||
:value="labelValue"
|
||
type="text"
|
||
class="host-workspace-dialog__input"
|
||
:placeholder="workspaceKind === 'project' ? '例如:客户 A 项目' : '例如:客户A项目'"
|
||
autocomplete="off"
|
||
:disabled="submitting"
|
||
@input="$emit('update:labelValue', ($event.target as HTMLInputElement).value)"
|
||
/>
|
||
</label>
|
||
|
||
<div v-if="errorMessage" class="host-workspace-dialog__error">{{ errorMessage }}</div>
|
||
</div>
|
||
|
||
<div class="host-workspace-dialog__actions">
|
||
<button type="button" class="host-workspace-dialog__btn ghost" :disabled="submitting" @click="$emit('close')">
|
||
取消
|
||
</button>
|
||
<button type="submit" class="host-workspace-dialog__btn primary" :disabled="submitting">
|
||
{{ submitting ? '创建中...' : (workspaceKind === 'project' ? '创建项目' : '创建工作区') }}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
defineOptions({ name: 'HostWorkspaceCreateDialog' });
|
||
|
||
const props = defineProps<{
|
||
open: boolean;
|
||
pathValue: string;
|
||
labelValue: string;
|
||
submitting?: boolean;
|
||
errorMessage?: string;
|
||
workspaceKind?: 'workspace' | 'project';
|
||
}>();
|
||
|
||
const emits = defineEmits<{
|
||
(event: 'close'): void;
|
||
(event: 'submit'): void;
|
||
(event: 'update:pathValue', value: string): void;
|
||
(event: 'update:labelValue', value: string): void;
|
||
}>();
|
||
|
||
const workspaceKind = props.workspaceKind || 'workspace';
|
||
|
||
const handleOverlayClose = () => {
|
||
if (props.submitting) return;
|
||
emits('close');
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.host-workspace-dialog-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 1300;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
background: var(--theme-overlay-scrim);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.host-workspace-dialog {
|
||
width: min(520px, 96vw);
|
||
border-radius: 18px;
|
||
border: 1px solid var(--theme-control-border-strong);
|
||
background: var(--theme-surface-card);
|
||
box-shadow: var(--theme-shadow-soft);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.host-workspace-dialog__header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 16px 18px;
|
||
border-bottom: 1px solid var(--theme-control-border);
|
||
}
|
||
|
||
.host-workspace-dialog__title {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: var(--claude-text);
|
||
}
|
||
|
||
.host-workspace-dialog__close {
|
||
border: 1px solid var(--theme-control-border-strong);
|
||
background: transparent;
|
||
color: var(--claude-text-secondary);
|
||
border-radius: 10px;
|
||
padding: 6px 12px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.host-workspace-dialog__close:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.host-workspace-dialog__body {
|
||
padding: 16px 18px;
|
||
display: grid;
|
||
gap: 12px;
|
||
}
|
||
|
||
.host-workspace-dialog__field {
|
||
display: grid;
|
||
gap: 6px;
|
||
}
|
||
|
||
.host-workspace-dialog__label {
|
||
font-size: 13px;
|
||
color: var(--claude-text-secondary);
|
||
}
|
||
|
||
.host-workspace-dialog__input {
|
||
border: 1px solid var(--theme-control-border-strong);
|
||
border-radius: 10px;
|
||
background: var(--theme-surface-soft);
|
||
color: var(--claude-text);
|
||
padding: 10px 12px;
|
||
font-size: 14px;
|
||
line-height: 1.4;
|
||
outline: none;
|
||
}
|
||
|
||
.host-workspace-dialog__input:focus {
|
||
border-color: var(--claude-accent);
|
||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--claude-accent) 20%, transparent);
|
||
}
|
||
|
||
.host-workspace-dialog__input:disabled {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.host-workspace-dialog__error {
|
||
font-size: 13px;
|
||
color: #ef4444;
|
||
}
|
||
|
||
.host-workspace-dialog__actions {
|
||
padding: 16px 18px;
|
||
border-top: 1px solid var(--theme-control-border);
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
}
|
||
|
||
.host-workspace-dialog__btn {
|
||
border-radius: 10px;
|
||
padding: 8px 14px;
|
||
border: 1px solid var(--theme-control-border-strong);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.host-workspace-dialog__btn.ghost {
|
||
background: transparent;
|
||
color: var(--claude-text-secondary);
|
||
}
|
||
|
||
.host-workspace-dialog__btn.primary {
|
||
background: var(--claude-accent);
|
||
border-color: var(--claude-accent-strong);
|
||
color: #fff;
|
||
}
|
||
|
||
.host-workspace-dialog__btn:disabled {
|
||
opacity: 0.65;
|
||
cursor: not-allowed;
|
||
}
|
||
</style>
|